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:

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

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.

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:

  1. !!c = c
  2. if c then t else e = if !c then e else t
  3. !(x < 0) = x >= 0
  4. if c then false else e = !c && e
  5. 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).