Functions as Abstractions
Let's motivate functions by looking at structural recursion over the natural numbers, and trying to capture in code the pattern we've used many times.
We have written a lot of structural recursions over the natural numbers. We started with code like
def stackedBoxes(count: Int): Image =
count match {
case 0 => Image.empty
case n => Image.square(20).beside(stackedBoxes(n-1))
}
and more recently have been writing code like
def polygonPoints(sides: Int, radius: Double): Image = {
val turn = (1.0 / sides).turns
def loop(count: Int): Image =
count match {
case 0 => Image.empty
case n =>
Image
.circle(5)
.at(Point(radius, turn * n))
.on(loop(n - 1))
}
loop(sides)
}
The details are different, but the underlying structure of the two examples is the same. In fact that's one of the points we're emphasizing: that code has underlying patterns that apply in many different situations.
When we see this repetition we might wonder if we can somehow capture it in code, so we don't have to write it out again and again. We're going to do this in this section, and in doing so we'll introduce functions.
Let's start by putting the two structural recursions next to each other, and in the case of polygonPoints
removing the surrounding code.
def stackedBoxes(count: Int): Image =
count match {
case 0 => Image.empty
case n => aBox.beside(stackedBoxes(n-1))
}
def loop(count: Int): Image =
count match {
case 0 => Image.empty
case n =>
Image
.circle(5)
.at(Point(radius, turn * n))
.on(loop(n - 1))
}
Keeping the common parts of these methods, and replacing those that vary with ???
, leaves us with something like
def aMethod(count: Int): Image =
count match {
case 0 => Image.empty
case n => ??? aMethod(count - 1)
}
This is the code skeleton for structural recursion over the natural numbers.
Now our challenge is to turn this into something we can actually use in Scala. Let's look at the recursive case in the code skeleton
case n => ??? aMethod(count - 1)
This is the only part that has a ???
. In the actual methods we have
case n => aBox.beside(stackedBoxes(n-1))
and
case n =>
Image
.circle(5)
.at(Point(radius, turn * n))
.on(loop(n - 1))
respectively.
To capture this in code we need to allow:
- varying how we build the result of the recursion (in one case it's
beside
, while we useon
in the other); and - the
Image
at the current step to depend onn
(as in the case taken frompolygonPoints
, where we use the valuen
to determine the location of the circle.)
In other words, the value we build is parameterized by the result of the recursion and the value n
. We could express this with a method
def build(n: Int, recursive: Image): Image = ???
and then, with two different implementations of build
, we could write both original methods in terms of
def aMethod(count: Int, build: ???): Image =
count match {
case 0 => Image.empty
case n => build(n, aMethod(count - 1, build))
}
However this won't work; we cannot pass a method as a parameter to a method. The solution, of course, is to use a function.
A function is basically a method, but we can use a function as a first-class value:
- we can pass it as an argument or parameter to a method or function;
- we can return it from a method or function; and
- we can give it a name using
val
.
Here's how we can solve the problem above using functions. First I'm going to define the method we've been calling aMethod
above. I'm going to call it fold
, which is the usual name for this kind of method. (This isn't an entirely correct implementation for fold, but we don't have all the tools to do it properly right now. This is something we'll come back to in a later chapter.)
def fold(count: Int, build: (Int, Image) => Image): Image =
count match {
case 0 => Image.empty
case n => build(n, fold(count - 1, build))
}
This method definition says that the parameter build
is a function from two arguments (an Int
and an Image
) to an Image
. To call fold
we need to create a function, so let's see an example of that.
val aBox = Image.square(20).fillColor(Color.royalBlue).strokeColor(Color.crimson)
val stack = (count: Int, image: Image) => aBox.beside(image)
We can now call fold
with stack
.
fold(5, stack)
This produes the output below.
Now that we've seen an example of functions, let's go into the details of defining and using them.
Function Literals
We've just seen an example of a function literal, which was
(count: Int, image: Image) => aBox.above(image)
The general syntax is an extension of this.
Function Literal Syntax
The syntax for declaring a function literal is
(parameter: type, ...) => expression
where
- the optional
parameter
s are the names given to the function parameters; - the
type
s are the types of the function parameters; and - the
expression
determines the result of the function.
The parentheses around the parameters are optional if the function has just a single parameter.
Function Types
To pass functions to methods we need to know how to write down their types (because when we declare a parameter we have to declare its type).
We write a function type like (A, B) => C
where A
and B
are the types of the parameters and C
is the result type.
The same pattern generalises from functions of no arguments to an arbitrary number of arguments.
Here's an example. We create a method that accepts a function, and that function is from Int
to Int
. We write this type as Int => Int
or (Int) => Int
.
def squareF(x: Int, f: Int => Int): Int =
f(x) * f(x)
We can define a function add42
, and pass it to this method
val add42 = (x: Int) => x + 42
squareF(0, add42)
// res3: Int = 1764
We could also pass a function literal
squareF(0, x => x + 42)
// res4: Int = 1764
Note that we didn't have to put the parameter type on the function literal in this case because Scala has enough information to infer the type.
Function Type Declaration Syntax
To declare a function type, write
(A, B, ...) => C
where
A, B, ...
are the types of the input parameters; andC
is the type of the result.
If a function only has one parameter the parentheses may be dropped:
A => B
Functions as Objects
All first class values are objects in Scala, including functions. This means functions can have methods, including some useful means for composition.
val addTen = (a: Int) => a + 10
val double = (a: Int) => a * 2
val combined = addTen.andThen(double) // this composes the two functions
combined(5)
// res5: Int = 30
Calling a function is actually calling the method called apply
on the function. Scala allows a shortcut for any object that has a method called apply
, where can drop the method name apply
and write the call like a function call. This means the following are equivalent.
val halve = (a: Int) => a / 2
halve(4)
// res6: Int = 2
halve.apply(4)
// res7: Int = 2
Converting Methods to Functions
Methods are very similar to functions, so Scala provides a way to convert functions to methods. If we follow a method name with a _
it will be converted to a function.
def times42(x: Int): Int =
x * 42
val times42Function = times42 _
times42Function(2)
// res8: Int = 84
We can also write a method call but replace all parameters with _
and Scala will convert the method to a function.
val times42Function2 = times42(_)
times42Function2(2)
// res9: Int = 84
Exercises
Exercise: Function Literals
Let's get some practice writing function literals. Write a function literal that:
- squares it's
Int
input; - has a
Color
parameter and spins the hue of thatColor
by 15 degrees; and - takes an
Image
input and creates four copies in a row, where each copy is rotated by 90 degrees relative to the previous image (use therotate
method onImage
to achieve this.)
The first function is
(x: Int) => x * x
// res10: Function1[Int, Int] = repl.MdocSession$MdocApp0$$Lambda$14360/0x000000010394c040@4175a36f
The second is
(c: Color) => c.spin(15.degrees)
// res11: Function1[Color, HSLA] = repl.MdocSession$MdocApp0$$Lambda$14361/0x000000010394d040@4b39f01b
The third is
(image: Image) =>
image.beside(image.rotate(90.degrees))
.beside(image.rotate(180.degrees))
.beside(image.rotate(270.degrees))
.beside(image.rotate(360.degrees))
// res12: Function1[Image, Image] = repl.MdocSession$MdocApp0$$Lambda$14362/0x000000010394d840@485e600b
Exercise: Function Types
Here's an interesting function we'll do more with in later sections. We don't need to understand what it does right now, though you might want to experiment with it.
val roseFn = (angle: Angle) =>
Point.cartesian((angle * 7).cos * angle.cos, (angle * 7).cos * angle.sin)
What is the type of the function roseFn
defined above? What does this type tell us?
The type is Angle => Point
. This means roseFn
is a function that takes a single argument of type Angle
and returns a value of type Point
. In other words, roseFn
transforms an Angle
to a Point
.