1. Overview
In this tutorial, we’ll learn about higher-order functions in Scala.
2. Higher-Order Functions
Simply put, we can say that a function is higher-order if it meets one or both of the following conditions:
- it takes one or more functions as parameters
- it returns a function
The fact that functions are first-class citizens in Scala makes this possible. For those who know Java 8+, it’s probably not an unfamiliar subject.
Let’s first see some examples using the most popular higher-order functions provided by Scala’s standard library. Then, we’ll create our own higher-order function.
3. Function as Parameter
Let’s begin by exploring some of Scala’s most popular higher-order functions. As we’ll see, each one takes a function as a parameter.
3.1. map
map is a function that transforms one collection into another collection. It’s our job to describe how to transform each element.
Suppose we’re given a sequence containing a few names, and our goal is to prefix each name with “sir”. We can achieve this quite easily using the map function:
val expected = Seq("sir Alex Ferguson", "sir Bobby Charlton", "sir Frank Lampard")
val names = Seq("Alex Ferguson", "Bobby Charlton", "Frank Lampard")
val sirNames = names.map(name => "sir " + name)
assertEquals(expected, sirNames)
In this case, we wrote an anonymous function explaining the transformation we desired. However, we could’ve used a properly defined function:
def prefixWithSir(name: String) = "sir " + name
val sirNames = names.map(prefixWithSir)
This way of passing a function parameter is especially useful when the function is more complex.
3.2. filter
filter allows us to clear a collection from elements we’re not interested in.
Let’s see how we’d only keep names that start with “John”:
val expected = Seq("John O'Shea", "John Hartson")
val names = Seq("John O'Shea", "Aiden McGeady", "John Hartson")
val johns = names.filter(name => name.matches("^John .*"))
assertEquals(expected, johns)
After calling the higher-order function and providing a proper predicate, we got a new sequence containing only the desired elements.
3.3. reduce
reduce is a bit different function than the previous ones. It combines multiple elements of the type T into a single one of type T.
Let’s see how we can use reduce to add together a list of earnings:
val expected = 2750
val earnings = Seq(1000, 1300, 450)
val sumEarnings = earnings.reduce((acc, x) => acc + x)
assertEquals(expected, sumEarnings)
Here, we have a sequence of earnings, and we want to calculate the sum of them. To do that, we need to provide a more complex function. That function consists of two elements: an accumulator, acc, and a current element, x.
An accumulator is responsible for keeping the state of calculations performed on our collection.
By default, reduce calls reduceLeft, which traverses the collection from left to right. If we want reversed order, we can use reduceRight.
3.4. fold
fold is very similar to reduce. The difference is, we can set the initial value for the accumulator. That also means we can control the type of the object to return.
Let’s say that we’ve got several strings and we want to count all their words:
val expected = 6
val strings = Seq("bunch of words", "just me", "it")
val numberOfWords = strings.foldLeft(0)((acc, x) => acc + x.split(" ").size)
assertEquals(expected, numberOfWords)
Please note that we’re using the foldLeft method. Similarly, as we saw with reduceLeft, calculations will start from the beginning of the collection. We’ve provided our own accumulator, 0, which is the first parameter of the function.
The rest is identical to reduce.
4. Functions Returning Functions
So far, we’ve discussed higher-order functions that take other functions as parameters. Now, it’s time to discover functions that return functions. To see this in action, let’s use Scala’s pattern matching API to write our own higher-order function that returns a function:
def mathOperation(name: String): (Int, Int) => Int = (x: Int, y: Int) => {
name match {
case "addition" => x + y
case "multiplication" => x * y
case "division" => x/y
case "subtraction" => x - y
}
}
def add: (Int, Int) => Int = mathOperation("addition")
def mul: (Int, Int) => Int = mathOperation("multiplication")
def div: (Int, Int) => Int = mathOperation("division")
def sub: (Int, Int) => Int = mathOperation("subtraction")
assertEquals(15, add(10, 5))
assertEquals(50, mul(10, 5))
assertEquals(2, div(10, 5))
assertEquals(5, sub(10, 5))
We’ve defined a mathOperation function, which takes the name of the operation as a parameter. Return type (Int, Int) => Int indicates that a function is returned — a function that performs the operation given by the name parameter.
Then, we’ve created function type variables – add, mul, div, and sub – and made sure that they work as expected.
This might look very similar to the factory design pattern we know from object-oriented programming.
5. Conclusion
In this tutorial, we’ve learned about higher-order functions in Scala. These are basic tools we can use in functional programming.
We can see that higher-order functions are quite powerful. They empower us to write simple, boilerplate-free, and readable code.
As usual, we can find code snippets over on GitHub.