1. Introduction

Lambda expressions also referred to as anonymous functions, are functions that are not bound to any identifier. They are much more convenient than full-fledged methods when we only need a function in one place. They are often passed as arguments to higher-order functions.

In this tutorial, we’re going to see how to use lambda expressions in Scala. We’ll first take a look at how to define them, and then we’ll look at some examples.

2. Lambda Expressions

Let’s start by defining a lambda expression that doubles a given number:

val double = (n: Int) => 2 * n
assert(double(10) == 20)

In the example above, the (n: Int) => 2 * n is declaring a function that takes an Int as an argument and returns an Int as a result. We can invoke it as we’d do with a normal function by calling double(10).

We can also define lambdas with more than one argument:

val sum = (x: Int, y: Int) => x + y
assert(sum(10, 20) == 30)

In this case, we defined a function with two arguments and called it as before.

2.1. Lambdas in Higher-Order Functions

Typically, we use lambda functions as arguments for higher-order functions, such as map. Let’s see how we can run an operation on the elements of a list using an anonymous function:

val ints = List(1, 2, 3, 4)
val doubled = ints.map((x: Int) => x * 2)
assert(doubled == List(2, 4, 6, 8))

In this example, we’re passing to map the same function that we defined above. Then, the implementation of map ensures that the lambda is applied to every element of the list. Note that, in this case, we cannot reuse our function. If we want to use it again, we’ll need to define it again, repeating its body. This is a direct consequence of the function not being bound to an identifier.

2.2. Syntactic Simplifications

There are some simplifications that we can leverage when passing anonymous functions as arguments. Such simplifications leverage Scala’s syntax and type inference to let us write an easier-to-read lambda, in particular:

  • Type inference lets us remove the explicit type for the parameter(s) of the lambda since the Scala compiler is capable of detecting it on its own
  • Scala’s syntax lets us avoid the name for the parameter of the function if we only use it at most once

Let’s see how they function looks like when we leverage type inference:

val ints = List(1, 2, 3, 4)
val doubled = ints.map(x => x * 2)
assert(doubled == List(2, 4, 6, 8))

In this case, we omitted the type Int. This is fine, as the compiler detects that ints are a List[Int], so of course, each element of the list will be an Int. Specifying that in the signature of the anonymous function is redundant, and therefore, we can omit it.

Also, since the parameter x only appears at most once, in the body of the lambda, we can omit its name:

val ints = List(1, 2, 3, 4)
val doubled = ints.map(_ * 2)
assert(doubled == List(2, 4, 6, 8))

However, we must use names if we reference the parameter more than once. For example, if we wrote ints.map(_ + _) then the compilation would fail with the error: “missing parameter type for expanded function ((x$1: <error>, x$2) => x$1.$plus(x$2))“, which is a complex way to tell us that the compiler could not understand what we were trying to do.

Finally, if the lambda consists of the one method call taking a single argument, we can omit the parameter entirely. So, for example, if we wanted to print the elements of ints above, we could just write ints.foreach(println).

2.3. Accepting a Lambda Expression as a Parameter

After seeing how we can use lambda expressions as actual parameters for higher-order functions, we can take a brief look at how the signature of a higher-order function looks like. First, let’s consider an object IntTransfomer allowing us to transform an Int into an instance of a generic A:

object IntTransformer {
  def transform[A](n: Int)(fun: Int => A): A = fun(n)

transform takes as a first argument a number to transform. Then, it inputs a function from Int to A.

We can use it as we did with map:

assert(IntTransformer.transform(123)(_.toBinaryString.length) == 7)

The call above computes the number of digits in the binary representation of the number 123 (which is 1111011). The anonymous function, in this case, is _.toBinaryString.length, expressed using the shortest form as we saw before. Here, we needed to make explicit the _ as a placeholder for the parameter. We could have avoided that by giving an identifier to our lambda expression:

val binaryDigits = (n: Int) => n.toBinaryString.length
assert(IntTransformer.transform(123)(binaryDigits) == 7)

Note, however, that in the solution above, we need to specify the type of the parameter of the lambda (n: Int), whereas we did not in the first solution (where the compiler would infer Int on our behalf).

2.4. Closure

Lambda expressions can “close” over the environment in which they are defined. This means that they can access variables that are not in their parameter list. Let’s see an example:

val multiplier = 3
val ints = List(1, 2, 3, 4)
val doubled = ints.map(_ * multiplier)
assert(doubled == List(3, 6, 9, 12))

In this case, the lambda uses multiplier, which is a local variable we defined in the outer environment. This is a powerful feature to avoid verbose lambda signatures. Nonetheless, this is very dangerous as well. If the lambda closes on mutable variables, it can modify them, thus adding side effects. Let’s see another example:

var count = 0
val ints = List(1, 2, 3, 4)
val doubled = ints.map{ i =>
  count = count + 1
  i * 2
assert(doubled == List(2, 4, 6, 8))
assert(count == 4)

In the code above, the lambda not only doubles the elements of the list but also counts them. It does that by modifying the count variable, on which the lambda closes. This is what we call “side-effect”: the function doesn’t communicate only using its parameters and returns the value but instead modifies the environment it’s defined in. Again, this is a very dangerous thing to do, as it makes it difficult to reason about the function.

3. Conclusion

In this article, we saw how to use lambda expressions in Scala. We took a look at how to pass them as arguments to higher-order functions and different ways to declare them.

As usual, the code is available over on GitHub.

Comments are open for 30 days after publishing a post. For any issues past this date, use the Contact form on the site.