Last updated on

Week 2 debrief: Higher-order functions, equational reasoning

Congratulations on completing your second week of CS-214! Here is a round-up of interesting questions, tips for exercises and labs, and general notes about the course.

Administrivia

Interesting links and Ed questions

What should product and allEven return for empty lists?

Here are two ways to think of these:

Simplicity

You could make these functions throw an exception for empty lists. Here is what this would look like for product:

def product(l: IntList):
  if l.isEmpty then throw new InvalidArgumentException("Empty!")
  else if l.tail.isEmpty then l.head
  else l.head * product(l.tail)

But! Perhaps we could make this nicer if we allowed ourselves to return a different value for empty lists. What should this value be?

def product(l: IntList):
  if l.isEmpty then ???
  else l.head * product(l.tail)

Let’s see by substitution:

product(IntCons(3, IntNil))
3 * product(IntNil)
3 * ???

So ??? should be such that 3 * ??? = 3. Hence, product(IntNil) should be 1!

Desirable properties

Another way to think of this issue is to think of what properties we want our functions to have:

How to return a function?

Let’s start with a small exercises:

def f(x: Int, y: Double)(z: Boolean): String = ""

2025-09-21/code/ReturningAFunction.worksheet.sc

What are the types of the following expressions?

Spoilers!
  • f has type (Int, Double) => Boolean => String, since it’s not applied to any arguments.
  • Applying f to 1 and 2.3 consumes the (Int, Double) => part of the function type, so f(1, 2.3) has type Boolean => String. At this point, we say that f is “partially applied”.
  • Applying f(1, 2.3) to true consumes the Boolean => part of the function type, so we get a plain String. At this point, we say that f is “fully applied”.
  • That’s a trap! We cannot write f(1, 2.3, true), because f takes just two arguments (an Int and a Double) before returning a new function (that function then takes one argument, a Bool, and returns a string).

Now, knowing this, let’s write a curried version of the add function. Normally, add has type (Int, Int) => Int. Given two numbers x and y, add(x, y) returns x + y. But! We want a version of add that can easily be partially applied. So we want to create an add with type Int => Int => Int. This can be written in any of the following ways:

val add: Int => Int => Int

2025-09-21/code/ReturningAFunction.worksheet.sc

def add(x: Int): Int => Int

2025-09-21/code/ReturningAFunction.worksheet.sc

def add(x: Int)(y: Int): Int

2025-09-21/code/ReturningAFunction.worksheet.sc

For the sake of the example, we let’s start with the second one. We take one int x: Int, and we must return a function Int => Int, so we create a function addX of that type and return it:

def add(x: Int): Int => Int =
  def addX(y: Int): Int =
    ???
  addX

2025-09-21/code/ReturningAFunction.worksheet.sc

Now, implementing addX is trivial.

def add(x: Int): Int => Int =
  def addX(y: Int): Int =
    x + y
  addX

2025-09-21/code/ReturningAFunction.worksheet.sc

This solution does what we want, but we can still do some refactoring to make it nicer. Let’s start by rewriting addX from def to a val, thus changing our implementation to use an anonymous function.

def add(x: Int): Int => Int =
  val addX = (y: Int) =>
    x + y
  addX

2025-09-21/code/ReturningAFunction.worksheet.sc

Now we can just inline addX to get:

def add(x: Int): Int => Int =
  (y: Int) => x + y

2025-09-21/code/ReturningAFunction.worksheet.sc

From there, we can easily change to the second form of add, above:

def add(x: Int)(y: Int): Int = x + y

2025-09-21/code/ReturningAFunction.worksheet.sc

Alternatively, can easily switch to the first form, with a val:

val add = (x: Int) => (y: Int) => x + y

2025-09-21/code/ReturningAFunction.worksheet.sc

That’s three ways to express the same functionality. Which one should we use in general? Read on to find out!

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