Functions as Values
The defining feature of a functional programming programming language is the ability to define functions that are first class values. Scala has special syntax for functions and function types. Here's a function that calculates
(a: Double, b: Double) => math.sqrt(a*a + b*b)
// res0: (Double, Double) => Double = <function2>
res0(3, 4)
// res1: Double = 5.0
Because Scala is an object oriented language, all first class values are objects. This means functions are objects, not methods! In fact, functions themselves have useful methods for composition:
(a: Int) => a + 10
// res0: Int => Int = <function1>
(a: Int) => a * 2
// res1: Int => Int = <function1>
res0 andThen res1 // this composes the two functions
// res2: Int => Int = <function1>
res2(5)
// res3: Int = 30
It may seem surprising and restrictive that Scala methods are not values. We can prove this by attempting to refer to a method without invoking it:
Color.rgb
// <console>:20: error: missing arguments for method rgb in object Color;
// follow this method with `_' if you want to treat it as a partially applied function
// Color.rgb
// ^
Fortunately, as the error message above suggests,
we can convert any method to a function using the _
operator
and call it with the same parameters:
Color.rgb _
// res4: (Int, Int, Int) => doodle.core.Color = <function3>
res4(255, 0, 0)
// res5: doodle.core.Color = ...
Higher Order Methods and Functions
Why are functions useful? We can already use methods to package up and name reusable fragments of code. What other advantages do we get from treating code as values?
- we can pass functions as parameters to other functions and methods;
- we can create methods that return functions as their results.
Let's consider the pattern from the concentric circles exercise as an example:
def manyShapes(n: Int): Image =
if(n == 1) {
singleShape
} else {
singleShape on manyShapes(n - 1)
}
def singleShape: Image = ???
This pattern allows us to create many different images
by changing the definition of singleShape
.
However, each time we provide a new definition of singleShape
,
we also need a new definition of manyShapes
to go with it.
We can make manyShapes
completely general by supplying
singleShape
as a parameter:
def manyShapes(n: Int, singleShape: Int => Image): Image =
if(n == 1) {
singleShape(n)
} else {
singleShape(n) on manyShapes(n - 1, singleShape)
}
Now we can re-use the same definition of manyShapes
to produce plain circles, circles of different hue,
circles with different opacity, and so on.
All we have to do is pass in a suitable definition of singleShape
:
// Passing a function literal directly:
val blackCircles: Image =
manyShapes(10, (n: Int) => Circle(50 + 5*n))
// Converting a method to a function:
def redCircle(n: Int): Image =
Circle(50 + 5*n) strokeColor Color.red
val redCircles: Image =
manyShapes(10, redCircle _)
<div class="callout callout-info"> Function Syntax
We're introducing a lot of syntax here! There's a dedicated section on function syntax in the quick reference if you get lost! </div>
Exercise: The Colour and the Shape
Starting with the code below, write color and shape functions to produce the following image:
def manyShapes(n: Int, singleShape: Int => Image): Image =
if(n == 1) {
singleShape(n)
} else {
singleShape(n) on manyShapes(n - 1, singleShape)
}
The manyShapes
method is equivalent to the
concentricCircles
method from previous exercises.
The main difference is that we pass in
the definition of singleShape
as a parameter.
Let's think about the problem a little. We need to do two things:
- write an appropriate definition of
singleShape
for each of the three shapes in the target image;
- call
manyShapes
three times, passing in the appropriate definition ofsingleShape
each time and putting the resultsbeside
one another.
Let's look at the definition of the singleShape
parameter in more detail.
The type of the parameter is Int => Image
,
which means a function that accepts an Int
parameter and returns an Image
.
We can declare a method of this type as follows:
def outlinedCircle(n: Int) =
Circle(n * 10)
We can pass a reference to this method to manyShapes
to create
an image of concentric black outlined circles:
manyShapes(10, outlinedCircle).draw
The rest of the exercise is just a matter of copying, renaming, and customising this function to produce the desired combinations of colours and shapes:
def circleOrSquare(n: Int) =
if(n % 2 == 0) Rectangle(n*20, n*20) else Circle(n*10)
(manyShapes(10, outlinedCircle) beside manyShapes(10, circleOrSquare)).draw
For extra credit, when you've written your code to
create the sample shapes above, refactor it so you have two sets
of base functions---one to produce colours and one to produce shapes.
Combine these functions using a combinator as follows,
and use the result of the combinator as an argument to manyShapes
def colored(shape: Int => Image, color: Int => Color): Int => Image =
(n: Int) => ???
<div class="solution">
The simplest solution is to define three singleShapes
as follows:
def manyShapes(n: Int, singleShape: Int => Image): Image =
if(n == 1) {
singleShape(n)
} else {
singleShape(n) on manyShapes(n - 1, singleShape)
}
def rainbowCircle(n: Int) = {
val color = Color.blue desaturate 0.5.normalized spin (n * 30).degrees
val shape = Circle(50 + n*12)
shape strokeWidth 10 strokeColor color
}
def fadingTriangle(n: Int) = {
val color = Color.blue fadeOut (1 - n / 20.0).normalized
val shape = Triangle(100 + n*24, 100 + n*24)
shape strokeWidth 10 strokeColor color
}
def rainbowSquare(n: Int) = {
val color = Color.blue desaturate 0.5.normalized spin (n * 30).degrees
val shape = Rectangle(100 + n*24, 100 + n*24)
shape strokeWidth 10 strokeColor color
}
val answer =
manyShapes(10, rainbowCircle) beside
manyShapes(10, fadingTriangle) beside
manyShapes(10, rainbowSquare)
However, there is some redundancy here:
rainbowCircle
and rainbowTriangle
, in particular,
use the same definition of color
.
There are also repeated calls to strokeWidth(10)
and
strokeColor(color)
that can be eliminated.
The extra credit solution factors these out into their own functions
and combines them with the colored
combinator:
def manyShapes(n: Int, singleShape: Int => Image): Image =
if(n == 1) {
singleShape(n)
} else {
singleShape(n) on manyShapes(n - 1, singleShape)
}
def colored(shape: Int => Image, color: Int => Color): Int => Image =
(n: Int) =>
shape(n) strokeWidth 10 strokeColor color(n)
def fading(n: Int): Color =
Color.blue fadeOut (1 - n / 20.0).normalized
def spinning(n: Int): Color =
Color.blue desaturate 0.5.normalized spin (n * 30).degrees
def size(n: Int): Double =
50 + 12 * n
def circle(n: Int): Image =
Circle(size(n))
def square(n: Int): Image =
Rectangle(2*size(n), 2*size(n))
def triangle(n: Int): Image =
Triangle(2*size(n), 2*size(n))
val answer =
manyShapes(10, colored(circle, spinning)) beside
manyShapes(10, colored(triangle, fading)) beside
manyShapes(10, colored(square, spinning))
</div>