Last updated on
Good to understand (week 2)
This Good to understand document provides answers to commonly asked questions.
This document does not contain new material. We encourage you to check out the table of contents and choose the sections that interest you.
Other than that, you are not required to read the entire document, as its content is already covered in the lectures, exercises or labs.
Sections marked with 📘 are alternative explanations of course material that is already covered in lectures, exercises or labs. Sections marked with 🚀 are here to satisfy your curiosity, but you will not be graded on them.
To curry or not to curry? The many ways to define a function
With higher-order-functions, we have seen lots of different ways to define functions. You now have two dimensions of choice:
- Currying VS not currying
- Named VS anonymous
There is also the option of defining your function as a val
or a def
.
Named VS Anonymous
Whether you name your function or not is a matter of succinctness.
Naming a function is advantageous when
- It will be called from many different places, or
- The functionality is complex enough to require a name
Often, we have very simple functionality that doesn’t require a name. The two following blocks of code show the same function written in an anonymous and named way respectively. Would you prefer reading this:
studentResults.filter(_.grade > 5)
Or this?
def hasGradeGreatedThan5(s: StudentResult) =
s.grade > 5
studentResults.filter(hasGradeGreaterThan5)
The second one is much longer and hence much more distracting to read.
In principle, you should use anonymous functions for small functions that are meant for a very specific and simple task that you are using in a single place. It makes the code smoother to read.
Currying or not currying, that is the question
Currying a function means transforming a function that takes multiple arguments to multiple functions that each take part of the arguments. For example, the following function:
def add(x: Int, y: Int): Int = x + y
can be curried this way:
def addCurried(x: Int)(y: Int): Int = x + y
This is useful when you want to create partially applied functions.
For example, we can use addCurried
to create a function that always add 5
to its parameter:
val add5 = addCurried(5) // 'add5' is a function of type (Int) => Int
println(add5(3)) // prints 8
Suppose you want to implement logging in your program. To do this, you implement this method:
def log(component: String, message: String): Unit =
println(s"[$component] $message")
Each part of your program uses a different value for component
, so that you know exactly from where each log message comes from.
But you end up with a lot of duplicated code. Within one part of your code, the component
value is the same for all log
calls. Isn’t there a way to simplify this? Yes, currying!
You redefine your function as:
def logCurried(component: String)(message: String): Unit =
println(s"[$component] $message")
Then, in each part of your code, you create a logger that only gets used there. For example, if you have a server component:
val logger = logCurried("SERVER")
// Later in the server code
logger("Received request")
This is what we mean when we say partially applied functions, and this is where currying shines.
We’ll see more examples of this throughout the course.
val
or def
Functions can be defined using either val
or def
, so which one should you use? For example, when defining a curried version of addition, is it better to write this def
?
def add(x: Int)(y: Int): Int = x + y
2025-09-21/code/ReturningAFunction.worksheet.sc
… or this val
?
val add = (x: Int) => (y: Int) => x + y
2025-09-21/code/ReturningAFunction.worksheet.sc
In practice, def
is the most commonly used: use it by default. When the function is needed repeatedly as a value, use val
. def
can also be used, but will take a small amount of time to translate to a value every time it is used as one, thus val
can provide slightly better performance in such cases. Thus, a good rule of thumb is to use def
when defining functions that will mostly be called directly, and val
for functions that will mostly be passed to other functions.
val numbers = List(1, 2, 3, 4, 5)
def add1(x: Int, y: Int): Int = x + y
object Math:
def add2(x: Int, y: Int): Int = x + y
@main def run() =
def internalAdd(x: Int, y: Int): Int = x + y
val add3: (Int, Int) => Int = (x: Int, y: Int)=> x + y
// All of these can be used in the same way
println(numbers.foldLeft(0)(add1))
println(numbers.foldLeft(0)(Math.add2))
println(numbers.foldLeft(0)(internalAdd))
println(numbers.foldLeft(0)(add3))
// val has slightly better performance if used repeatedly as a value
val numbers2 = List(6, 7, 8, 9, 10)
val numbers3 = List(11, 12, 13, 14, 15)
println(numbers.foldLeft(0)(add3))
println(numbers2.foldLeft(0)(add3))
println(numbers3.foldLeft(0)(add3))
There is one case in which def
versus val
makes a major difference, however: when the definition itself has side effects. val
is evaluated immediately; def
is delayed until it is called.
For example, here are four code fragments. Can you say what each of them prints?
val add1v =
println("Oh! A val!");
(x: Int) => x + 1
2025-09-21/code/ValDef.worksheet.sc
def add1d =
println("Oh, a `def`!");
(x: Int) => x + 1
2025-09-21/code/ValDef.worksheet.sc
add1v(2)
2025-09-21/code/ValDef.worksheet.sc
add1d(2)
2025-09-21/code/ValDef.worksheet.sc
Spoilers!
val add1v =
println("Oh! A val!");
(x: Int) => x + 1
// Prints "Oh! A val!"
2025-09-21/code/ValDef.worksheet.sc
def add1d =
println("Oh, a `def`!");
(x: Int) => x + 1
// Does not print anything
2025-09-21/code/ValDef.worksheet.sc
add1v(2)
// Returns 3, does not print anything
2025-09-21/code/ValDef.worksheet.sc
add1d(2)
// Prints "Oh, a `def`!", then returns 3
2025-09-21/code/ValDef.worksheet.sc
How does equals(obj: AnyRef)
work? 📘
Short answer is, it depends on the objects you are comparing.
-
For collections, such as
List
s andSet
s, first the types of the two collections is compared (aSet
is never equal to aList
, even if they have the same elements), then the two collections are compared element-wise. You can find more details on this page of the official Scala documentation. -
For
case class
es, the comparison is structural rather than by reference. This means that if two different instances of acase class
contain the same data, they are equal, even though they are stored at two different places in memory. You can see a concret example in the Scala documentation. -
For classes (which are not
case class
es), the comparison is performed by reference, similarly to Java. Note, however, that suchclass
es are rarely used in this course.
Using equational reasoning to simplify allPositiveOrZero
In the recursion exercise set, some of you ended up with the following definition for allPositiveOrZero
:
def allPositiveOrZero(l: IntList): Boolean =
if !l.isEmpty then
if l.head < 0 then false
else allPositiveOrZero(l.tail)
else true
2025-09-21/equational.worksheet.sc
That’s pretty heavy! There are two nested if ... then ... else ...
blocks in there.
Let’s see how we might improve this a bit, using equational reasoning.
What is equational reasoning? In Algebra, if we know that the following identity is true: $$ (a + b)^2 = a^2 + 2ab + b^2 $$ then we can immediately know that $$ (xz + y^2)^2 = (xz)^2 + 2xzy^2 + (y^2)^2 $$ is also true, by substituting in $a \mapsto xz$ and $b \mapsto y^2$. We can do the same thing when writing functional programs!
Here are a few identities that we can show, with Boolean values:
!!c
=c
if c then t else e
=if !c then e else t
!(x < 0)
=x >= 0
if c then false else e
=!c && e
if c then e else true
=???
(try to fill in the blank!)
Do you know how to prove these? (Hint: remember truth tables, from ICC?)
Now, back to allPositiveOrZero
. Can you use any of the identities above to simplify the code?
Step 1
if l.head < 0 then false
else allPositiveOrZero(l.tail)
2025-09-21/equational.worksheet.sc
is exactly our identity (4), if we substitute c
with l.head < 0
and e
with allPositiveOrZero(l.tail)
.
We can then immediately rewrite our equation into !(l.head < 0) && allPositiveOrZero(l.tail)
.
What’s the next step?
Step 2
Going one step further, identity (2) now also applies to !(l.head < 0)
, so we can further rewrite the function into
def allPositiveOrZero2(l: IntList): Boolean =
if !l.isEmpty then
l.head >= 0 && allPositiveOrZero2(l.tail)
else true
2025-09-21/equational.worksheet.sc
Can you simplify it further, eliminating the remaining if
expression?
Step 3
Identity (5) can be written as if c then e else true
= !c || e
.
Substituting c
with !l.isEmpty
and e
with (l.head >= 0 && allPositiveOrZero(l.tail))
(notice the ()
! One should always be careful about substitutions,
in order to not introduce some confusion between operators,
just like in algebra), and we get
!!l.isEmpty || (l.head >= 0 && allPositiveOrZero(l.tail))
.
One more step?
Step 4
We can apply (1) as well, resulting in our final code
def allPositiveOrZero3(l: IntList): Boolean =
l.isEmpty || (l.head >= 0 && allPositiveOrZero3(l.tail))
2025-09-21/equational.worksheet.sc
Pretty short and sweet! And think of what this code says… it’s exactly the English spec! “A list is all positive or zero if either it is empty, or its first element is positive or zero, and all remaining elements are all positive or zero.”
Boids UI server unreachable in WSL 🚀
You may have encountered this issue while working on the Boids lab if you use WSL on a Windows machine. When following the instructions to run and reach the Boids web UI, the browser is not able to get an answer from the server. Why could that be?
Some of you put on their debugging hats and found out the server was reachable from within WSL (congrats!), for example by using curl
on the command line.
If the server is running, why can’t the browser reach it? It has to do with how the server is configured and how WSL 2 works.
By default, the Boids UI server listens on 127.0.0.1
, which is a local-only IP address. In short, the server only listens for requests coming from within WSL. Programs running within WSL can reach the server without problem.
However, your browser is running on Windows, not in WSL. Because WSL is a lightweight virtual machine, requests coming from Windows are treated as requests coming from another machine (since WSL is virtually another machine), thus requests from your browser are not local and the server won’t receive them.
To fix this, we provided you with an updated code and a new command to run for WSL:
CS214_HOST=0.0.0.0 sbt
The part before sbt
tells the server to listen on 0.0.0.0
instead of 127.0.0.1
. Listening on 0.0.0.0
means the server will receive all requests, not only the ones from programs within WSL.
Why didn’t we simply configure the server to always listen on 0.0.0.0
?
In most setups (Linux, macOS, running the lab directly on Windows instead of WSL), listening on 127.0.0.1
is fine, because your browser runs in the same environment as the server. In such cases, listening only to local requests makes sense, because everything runs in the same environment (the server and your browser).