Exploring properties of function composition in Kotlin

Function composition is a technique where you create a new function by applying the result of one function to another. I've sometimes heard this described as "chaining" multiple functions together.

Assuming two functions \(f(x)\) and \(g(x)\), we can compose a new function \(h(x)\) by passing the result of \(g(x)\) to \(f(x)\)

\[h(x)=f(g(x))\]

Another notation for \( f(g(x)) \) is \(f \circ g(x) )\) ( or \(f \circ g\) omitting the parameter), read as \(f\) of \(g\) of \(x\), or \(f\) after \(g\) of \(x\).

We can begin to describe this as code by declaring f and g functions. Here f is a function that accepts a Double an returns a String, while g is a function that accepts an Integer and returns a Double.

val f: (Double) -> String = { x ->
    "$x"
}

val g: (Int) -> Double = { x ->
    x * 1.0
}

We can compose these functions by declaring a new function h that applies the result of f and then g. This creates a new function that accepts an Integer and returns a String.

val h: (Int) -> String = { x -> f(g(x)) }

By composing the functions we've created a new function that's a direct mapping from Integer to String.

flowchart LR Int-- g -->Double Double-- f -->String Int-- h -->String

We can create a higher order function to generify the process of composing functions easier. Note that the order of the parameters is resolved right to left to match the mathematical notation.

fun <T, U, V> compose(fn2: (U) -> V, fn1: (T) -> U): (T) -> V {
    return { fn2(fn1(it)) }
}

and refactor the previous example to use compose to create h

val h: (Int) -> String = compose(f, g)

Category Theory for Programmers[1] describes two properties that composition in any category must satisfy.

  1. "Composition is associative" - Regardless of how they are grouped by parenthesis, the result should always be the same[2].

\[ f \circ (g \circ h) = (f \circ g) \circ h = f \circ g \circ h \]

Let's update our example to demonstrate this

val f: (Double) -> String = { x ->
    "$x"
}

val g: (Int) -> Double = { x ->
    x * 1.0
}

val h: (List<Int>) -> Int = {
    it.size
}

We now have 3 functions f, g, and h which have their corresponding implementations. Composing the functions in any pair gives us the same result.

val `f o g o h`: (List<Int>) -> String = {
    f(g(h(x)))
}

val `(f o g) o h`: (List<Int>) -> String = compose(compose(f, g), h)

val `f o (g o h)`: (List<Int>) -> String = compose(f, compose(g, h))
  1. "For every object A there is an arrow which is a unit of composition. This arrow loops from the object to itself" - This describes an identity function.

\[ f \circ \text{id}_A = f\]

or

\[ \text{id}_B \circ f = f\]

We can describe an identity \( \text{id}_A \), read identity on A as a mapping to itself.

flowchart TD A-- id -->A

In code, it's a function that returns the value that is passed to it

fun <T> id(x: T): T {
    return x
}

When composed with another function, we can see that the result is the function it was composed with as expected.

val `f o id`: (Double) -> String = compose(f, ::id)
val `id o f`: (Double) -> String = compose(::id, f)

  1. Category: The essence of composition (2014) Bartosz Milewski’s Programming Cafe. Available at: https://bartoszmilewski.com/2014/11/04/category-the-essence-of-composition/ (Accessed: January 22, 2024). ↩︎

  2. If you are interested in proofs, this Stackoverflow thread breaks it down in a couple of different ways. ↩︎

Subscribe to Another Dev's Two Cents

Don’t miss out on the latest issues. Sign up now to get access to the library of members-only issues.
jamie@example.com
Subscribe