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:
f
is the name of the function input parameter. It’s just like naming aString
parameters
or anInt
parameteri
.- The type signature of
f
specifies the type of the functions this method will accept. - The
()
portion off
’s signature (on the left side of the=>
symbol) states thatf
takes no input parameters. - The
Unit
portion of the signature (on the right side of the=>
symbol) indicates thatf
should not return a meaningful result. - Looking back at the body of the
sayHello
method (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.