1. Introduction

We can think of function composition as the application of several functions, one after the other, to one or more values.

In this tutorial, we’re going to see two ways to use function composition in Scala. We’ll start with a theoretical definition of composition and then see how to implement it.

2. What’s Function Composition

Before diving into function composition in Scala, let’s look at a bit of math and define function composition.

Given two functions, f: X -> Y and g: Y -> Z, we can define their composition as a function h = g f : X -> Z, where h(x) = g(f(x)). In other words, h is a function that maps elements of the domain X in the codomain Z. It takes f to map an element of X into one of Y, which is the domain of g. It then takes g to obtain an element of Z.

There are many ways to read the notation g ∘ f. The most common are “g composed with f” and “f then g“.

Function composition has some nice properties. Namely, function composition is always associative: f  (g h) = (f g) h.

Commutativity, on the other hand, is a property attained only by particular functions, and often in special circumstances. For example, f(x) = x ^ 2 and g(x) = x ^ 3 commute, as f(g(x)) = g(f(x)) = x ^ 6. On the other hand, f(x) = x + 3 and g(x) = |x| (absolute value of x) only commute when x >= 0.

3. Composing Functions in Scala

In Scala, the trait Function1[T1, R] defines methods to compose functions. Function1 models unary functions, that is, functions with a single parameter (T1 in the trait definition), producing a value of type R.

There are two ways to compose such functions, according to Function1: compose and andThen. The difference between the two relies on the order of application. Given two unary functions f and g, f andThen g applies first f and then g. On the contrary, f compose g applies first g and then f.

3.1. andThen

Let’s see how Function1[T1, R] defines andThen:

def andThen[A](g: R => A): T1 => A = { x => g(apply(x)) }

andThen inputs a Function1[R, A], that is a function from R to a new type A. The result is a function from T1 to A: this means that, given f: Function1[T1, R] and g: Function1[R, A], f.andThen(g) produces a function with the same domain of f, T1, and the codomain of g, A.

Let’s see an example:

val f = (x: Float) => x.abs
val g = (x: Float) => x + 3
val h1 = f andThen g
val h2 = g andThen f

assert(h1(-1) == 4f)
assert(h2(-1) == 2f)

The example above shows that the two functions are applied in a different order. h1(-1) is equivalent to g(f(-1)), which is equal to 4. h2(-1), on the other hand, is equivalent to f(g(-1)), which is equal to 2.

3.2. compose

Let’s see how Function1[T1, R] defines compose:

def compose[A](g: A => T1): A => R = { x => apply(g(x)) }

compose inputs a Function1[A, T1], that is a function from a new type A to T1. The result is a function from A to R: this means that given f: Function1[T1, R] and g: Function1[A, T1],  f.compose(g) produces a function with the same domain of g, A, and the codomain of f, R. In other words, this is the opposite of andThen.

Let’s see an example:

val f = (x: Float) => x.abs
val g = (x: Float) => x + 3
val h1 = f compose g
val h2 = g compose f

assert(h1(-1) == 2f)
assert(h2(-1) == 4f)

The example above shows that compose behaves with a different order of application than andThen. h1(-1) is now the same as f(g(-1)), which is equal to 2. On the other hand, h2(-1) is equivalent to g(f(-1)), which returns 4.

3.3. Composing Functions With More Than One Parameter

So far we’ve seen function composition applied on unary functions. It’s now time to see how we can extend this concept to other types of functions, such as binary ones (two parameters instead of one).

Scala models binary functions using the Function2[T1, T2, R] trait, where T1 and T2 are the types of the parameters and R is, as before, the type of the returned valued. However, Function2 does not define either andThen or compose.

Still, Function2 defines a method named tupled:

def tupled: Tuple2[T1, T2] => R = {
  case Tuple2(x1, x2) => apply(x1, x2)

tupled basically turns a function of type T1 => T2 => R into a function of type (T1, T2) => R. In other words, instead of inputting two separate arguments (one for T1 and one for T2), the new function inputs a single argument, which is a tuple of two elements (a Tuple2, in Scala). This way we can get a unary function out of a binary one. Hence, we can easily use function composition as we saw before:

val f = (x: Int, y: Int) => (x + 1, y + 2)
val g = (x: Int, y: Int) => x - y
val h = f.tupled andThen g.tupled

assert(h((5, 4)) == 0)

In the example above f is a binary function that returns a pair. Even if f returns two numbers, from Scala’s point of view that is a single value of type Tuple2[Int, Int]. g is also a binary function, so we cannot compose them as-is. Instead, we have to call tupled on both of them. We can think of f.tupled and g.tupled in the following way:

val ftupl = (t: (Int, Int)) => (t._1 + 1, t._2 + 2)
val gtupl = (t: (Int, Int) ) => t._1 - t._2

The types and the bodies are a little more complex to read, but they are equivalent to f and g, respectively. Still, ftupl and gtupl are unary functions, and hence we can call andThen on them and define h.

If we had a single binary function (either f or g), we could apply the same mechanism. Let’s see an example:

val f1 = (x: Int, y: Int) => x + y
val g1 = (x: Int) => x + 1
val h1 = f1.tupled andThen g1
assert(h1((5, 4)) == 10)

val f2 = (x: Int) => (x + 1, x - 1)
val g2 = (x: Int, y: Int) => x * y
val h2 = f2 andThen g2.tupled
assert(h2(2) == 3)

In the example above, only f1 and g2 are binary functions, whereas g1 and f2 are unary ones. In this case, we can apply tupled only where the arguments are more than one.

4. Conclusion

In this tutorial, we saw how to use function composition in Scala. Starting from the mathematical definition, we looked into Function1, representing unary functions in Scala, and saw how the standard library defines compose and andThen, and to use them. We also extended the concept to binary functions.

As usual, you can find the code over on GitHub.

Comments are open for 30 days after publishing a post. For any issues past this date, use the Contact form on the site.