A higher-order function (HOF) is often defined as a function that (a) takes other functions as input parameters or (b) returns a function as a result. In Scala, HOFs are possible because functions are first-class values.
As an important note, while we use the common industry term “higher-order function” in this document, in Scala this phrase applies to both methods and functions. Thanks to Scala’s Eta Expansion technology, they can generally be used in the same places.
From consumer to creator
In the examples so far in this book you’ve seen how to be a consumer of methods that take other functions as input parameters, such as using HOFs like map and filter.
In the next few sections you’ll see how to be a creator of HOFs, including:
- How to write methods that take functions as input parameters
- How to return a function from a method
In the process you’ll see:
- The syntax you use to define function input parameters
- How to call a function once you have a reference to it
As a beneficial side effect of this discussion, once you’re comfortable with this syntax, you’ll use it to define function parameters, anonymous functions, and function variables, and it also becomes easier to read the Scaladoc for higher-order functions.
Understanding filter’s Scaladoc
To understand how higher-order functions work, it helps to dig into an example.
For instance, you can understand the type of functions filter accepts by looking at its Scaladoc.
Here’s the filter definition in the List[A] class:
def filter(p: A => Boolean): List[A]
This states that filter is a method that takes a function parameter named p.
By convention, p stands for a predicate, which is just a function that returns a Boolean value.
So filter takes a predicate p as an input parameter, and returns a List[A], where A is the type held in the list; if you call filter on a List[Int], A is the type Int.
At this point, if you don’t know the purpose of the filter method, all you’d know is that its algorithm somehow uses the predicate p to create and return the List[A].
Looking specifically at the function parameter p, this part of filter’s description:
p: A => Boolean
means that whatever function you pass in must take the type A as an input parameter and return a Boolean.
So if your list is a List[Int], you can replace the type parameter A with Int, and read that signature like this:
p: Int => Boolean
Because isEven has this type—it transforms an input Int into a resulting Boolean—it can be used with filter.
Writing methods that take function parameters
Given that background, let’s start writing methods that take functions as input parameters.
Note: To make the following discussion clear, we’ll refer to the code you’re writing as a method, and the code you’re accepting as an input parameter as a function.
A first example
To create a method that takes a function parameter, all you have to do is:
- In your method’s parameter list, define the signature of the function you want to accept
- Use that function inside your method
To demonstrate this, here’s a method that takes an input parameter named f, where f is a function:
def sayHello(f: () => Unit): Unit = f()
This portion of the code—the type signature—states that f is a function, and defines the types of functions the sayHello method will accept:
f: () => Unit
Here’s how this works:
fis the name of the function input parameter. It’s just like naming aStringparametersor anIntparameteri.- The type signature of
fspecifies the type of the functions this method will accept. - The
()portion off’s signature (on the left side of the=>symbol) states thatftakes no input parameters. - The
Unitportion of the signature (on the right side of the=>symbol) indicates thatfshould not return a meaningful result. - Looking back at the body of the
sayHellomethod (on the right side of the=symbol), thef()statement there invokes the function that’s passed in.
Now that we’ve defined sayHello, let’s create a function to match f’s signature so we can test it.
The following function takes no input parameters and returns nothing, so it matches f’s type signature:
def helloJoe(): Unit = println("Hello, Joe")
Because the type signatures match, you can pass helloJoe into sayHello:
sayHello(helloJoe) // prints "Hello, Joe"
If you’ve never done this before, congratulations:
You just defined a method named sayHello that takes a function as an input parameter, and then invokes that function in its method body.
sayHello can take many functions
It’s important to know that the beauty of this approach is not that sayHello can take one function as an input parameter; the beauty is that it can take any function that matches f’s signature.
For instance, because this next function takes no input parameters and returns nothing, it also works with sayHello:
def bonjourJulien(): Unit = println("Bonjour, Julien")
Here it is in the REPL:
scala> sayHello(bonjourJulien)
Bonjour, Julien
This is a good start. The only thing to do now is see a few more examples of how to define different type signatures for function parameters.
The general syntax for defining function input parameters
In this method:
def sayHello(f: () => Unit): Unit
We noted that the type signature for f is:
() => Unit
We know that this means, “a function that takes no input parameters and returns nothing meaningful (given by Unit).”
To demonstrate more type signature examples, here’s a function that takes a String parameter and returns an Int:
f: String => Int
What kinds of functions take a string and return an integer? Functions like “string length” and checksum are two examples.
Similarly, this function takes two Int parameters and returns an Int:
f: (Int, Int) => Int
Can you imagine what sort of functions match that signature?
The answer is that any function that takes two Int input parameters and returns an Int matches that signature, so all of these “functions” (methods, really) are a match:
def add(a: Int, b: Int): Int = a + b
def subtract(a: Int, b: Int): Int = a - b
def multiply(a: Int, b: Int): Int = a * b
As you can infer from these examples, the general syntax for defining function parameter type signatures is:
variableName: (parameterTypes ...) => returnType
Because functional programming is like creating and combining a series of algebraic equations, it’s common to think about types a lot when designing functions and applications. You might say that you “think in types.”
Taking a function parameter along with other parameters
For HOFs to be really useful, they also need some data to work on.
For a class like List, its map method already has data to work on: the data in the List.
But for a standalone HOF that doesn’t have its own data, it should also accept data as other input parameters.
For instance, here’s a method named executeNTimes that has two input parameters: a function, and an Int:
def executeNTimes(f: () => Unit, n: Int): Unit =
for (i <- 1 to n) f()
def executeNTimes(f: () => Unit, n: Int): Unit =
for i <- 1 to n do f()
As the code shows, executeNTimes executes the f function n times.
Because a simple for loop like this has no return value, executeNTimes returns Unit.
To test executeNTimes, define a method that matches f’s signature:
// a method of type `() => Unit`
def helloWorld(): Unit = println("Hello, world")
Then pass that method into executeNTimes along with an Int:
scala> executeNTimes(helloWorld, 3)
Hello, world
Hello, world
Hello, world
Excellent.
The executeNTimes method executes the helloWorld function three times.
As many parameters as needed
Your methods can continue to get as complicated as necessary.
For example, this method takes a function of type (Int, Int) => Int, along with two input parameters:
def executeAndPrint(f: (Int, Int) => Int, i: Int, j: Int): Unit =
println(f(i, j))
Because these sum and multiply methods match that type signature, they can be passed into executeAndPrint along with two Int values:
def sum(x: Int, y: Int) = x + y
def multiply(x: Int, y: Int) = x * y
executeAndPrint(sum, 3, 11) // prints 14
executeAndPrint(multiply, 3, 9) // prints 27
Function type signature consistency
A great thing about learning about Scala’s function type signatures is that the syntax you use to define function input parameters is the same syntax you use to write function literals.
For instance, if you were to write a function that calculates the sum of two integers, you’d write it like this:
val f: (Int, Int) => Int = (a, b) => a + b
That code consists of the type signature:
val f: (Int, Int) => Int = (a, b) => a + b
-----------------
The input parameters:
val f: (Int, Int) => Int = (a, b) => a + b
------
and the body of the function:
val f: (Int, Int) => Int = (a, b) => a + b
-----
Scala’s consistency is shown here, where this function type:
val f: (Int, Int) => Int = (a, b) => a + b
-----------------
is the same as the type signature you use to define a function input parameter:
def executeAndPrint(f: (Int, Int) => Int, ...
-----------------
Once you’re comfortable with this syntax, you’ll use it to define function parameters, anonymous functions, and function variables, and it becomes easier to read the Scaladoc for higher-order functions.