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?

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:

Colours and Shapes

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:

  1. write an appropriate definition of singleShape for each of the three shapes in the target image;
  1. call manyShapes three times, passing in the appropriate definition of singleShape each time and putting the results beside 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

Many outlined circles

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

Many outlined circles beside many circles and squares

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>

Take Home Points→