7 minutes read

As you know, high-order functions (HOF) give us the opportunity to write generic code. But let's look at the following Account class:

case class Account(id: String, login: String, name: String)

If we want to transform a list of accounts into a list of its identifiers, we can use the map method, but to get id from the account, we need an additional function:

def getId(account: Account): String = account.id

def setIds(accounts: List[Account]): Set[String] = 
  accounts.map(getId).toSet

Even though the logic here is simple, the code seems to be overloaded. Let's get acquainted with anonymous functions in Scala that facilitate our work with HOF!

Lambda

Knowing the context of the HOF used, we can pass a block of code named anonymous function or lambda function as an argument to a higher-order function. The definition of a lambda function is not bound to an identifier like in the case with val or def definitions — we can define lambda anywhere we need.

Let's look at the lambda syntax:

(variable1: Type1, variable2: Type2, ...) => { /* expression */ }

The => sign is known as a transformer. It's used to transform the parameters of the left-hand side of the symbol into a new result using the expression on the right-hand side.

Let's apply this knowledge and create some lambdas:

val numbers = List(1, 2, 3, 4, 5)

// single lambda for multiplication  
numbers.map { (x: Int) => x * 2 } // List(2, 4, 6, 8, 10)

// lambda with several operations inside the block code
numbers.map { (x: Int) => 
  val result: Int = x * 2 + 1
  println(s"[DEBUG] got $result from $x")
  result
} // List(3, 5, 7, 9, 11)

Consider that the lambda body can contain local variables, nested functions, and a lot of code strings. If your lambda is becoming too big and complicated, defining it as a classical named function would be a good idea.

The Scala compiler always checks lambda types, so you don't have to worry about accidentally making a mistake in them.

For instance:

numbers.map { (x: Int, y: Int) => x + y } // does not compile (one parameter function is expected) 
numbers.reduce { (x: Int, y: Int) => x + y } // 15

Shortening anonymous functions

Since the Scala compiler can infer data types, you can remove type declaration from our explicit long syntax:

numbers.map { (x) => x * 2 } 

If you have only one argument, you can skip the parentheses around it:

numbers.map { x => x * 2 } 

Also, in Scala, we use the special symbol _ instead of a variable name when the parameter appears only once in your function:

numbers.map { _ * 2 } 

When using more than one parameter, apply every underscore symbol for every parameter you use:

numbers.foldLeft("Digits = ") { (number, accumulator) => accumulator + number } // Digits = 12345

numbers.foldLeft("Digits = ") { _ + _ } // Digits = 12345

We used the first underscore symbol as a number parameter, and the second one as an accumulator parameter.

Let's use this knowledge in our function with accounts:

// just lambda
def setIds(accounts: List[Account]): Set[String] = 
  accounts.map((account: Account) => account.id).toSet

// lambda with the derived type
def setIds(accounts: List[Account]): Set[String] = 
  accounts.map(account => account.id).toSet

// simplified lambda with the derived type
def setIds(accounts: List[Account]): Set[String] = 
  accounts.map(_.id).toSet

Closures

What if we want to add some additional parameters from the current context to our lambda? For instance, we need to add the prefix parameter to the lambda function account => account.id.

Do you think this code will compile?

def setIds(accounts: List[Account]): Set[String] = 
  accounts
    .map(account => prefix + account.id)
    .toSet

Of course, it will not, because we didn't define our new parameter prefix. That one's clear, but what about this case below?

def setIds(accounts: List[Account], prefix: String): Set[String] =
  accounts
    .map(account => prefix + account.id)
    .toSet

Surprisingly, it works! The lambda function account => prefix + account.id, just like a regular function, can use values, functions, classes, etc., defined in the scope where it's located. This type of lambdas captured by values from the outside are also called closures.

Conclusion

Great! Now you know that if a HOF needs a function as an argument, you can pass this logic as a short lambda expression without any additional definition. You can apply the abbreviated lambda syntax using underscores, and you also are aware of the lambda function's ability to capture variables from the current context. Now you can see why lambda functions are valuable and practical tools in functional programming. Let's put this into practice!

How did you like the theory?
Report a typo