Learn Function Composition By Building compose and composeAll Utility Functions

9 min read

The ability to compose functions allows you to solve complex problems by breaking them down into small, atomic operations and composing those pieces together. Those same pieces can then be composed in new ways to solve other problems. As we'll see in this post, JavaScript's first-class function support gives us the tools we need to implement composition utilities. Once we've gone through the process step-by-step, you'll understand function composition well enough to start using it in your code. The more you use it, the more opportunities will start jumping out at you. So let's get started!

Calling Multiple Functions With Manual Composition

Let's start with some initial setup. We'll define a handful of functions that we'll use throughout this explanation and we'll briefly look at ways to combine them without our composition utilities.

Our Cast of Characters

The functions we're going work with are fairly simple so we won't have to spend too much processing power thinking about how they work and we'll be able to quickly figure out the expected result in our heads.

add

add is a binary function, meaning it takes 2 arguments. It accepts 2 numbers and returns the sum.

const add = (a, b) => a + b

inc

inc is a unary function, meaning it takes a single argument. It takes in one number and returns the value, incremented by 1.

const inc = (a) => a + 1

dbl

dbl is another unary function that takes in a number and returns that value multiplied by two.

const dbl = (a) => a * 2

Using The Functions Together

Now that we have a handful of straightforward functions to work with, let's look at how we might get a result that needs to use them all.

Each Function as a Step In The Process

Let's say we want to add two numbers, then increment the result, then finally double the result of that. We might do something like this:

const added = add(1, 2) // 3

const incremented = inc(added) // 4

const doubled = dbl(incremented) // 8

With this approach, we have code that is nice and easy to follow, everything flows in order and we get the desired result. The problem is that we need intermediate variables along the way and naming things is hard. In any non-trivial code, you're likely to have more complex processes to build, so this can get out of hand quickly.

Nesting The Function Calls

If we wanted to eliminate the intermediate variables, we could write something more like this:

const doubled = dbl(inc(add(1, 2))) // 8

We've eliminated the extra variable names, but the readability of the code has suffered quite a bit.

We could wrap this in a new function like so:

function addIncDbl(a, b) {
  return dbl(inc(add(a, b)))
}

addIncDbl(1, 2) // 8

This gives us a function name that communicates the intent of the code and the less readable code has been tucked away inside that utility. You could argue this is better, but it is still far from ideal.

Luckily, function composition gives us a way to write this same code in a cleaner way. First, we need to implement our compose and composeAll utilities.

Implementing the compose Utility

We'll start by combining our first two functions, add and inc. So we want a single function that adds two numbers, then increments the result. We want to build this up using our existing functions.

The Goal

The goal is to write code that looks like this:

const addInc = compose(inc, add)

addInc(1, 2) // 4

The result of running compose with inc and add would be a left-right composition, giving us a function that internally would apply add to our arguments, then apply inc to the result of add. So 1 and 2 get passed to add then the result, 3 is then passed to inc resulting in 4.

Let's build it!

Since JavaScript gives us first-class functions, we can pass functions around as values, so in compose(inc, add), Both add and inc are function references being passed as arguments to the compose function.

For defining the parameters in our generic compose function, let's call them f and g.

const compose = (f, g) => /* Magic happens... */

Once we've accepted our 2 functions, we need to return a function

const compose = (f, g) => () => /* Magic happens... */

That returned function needs to accept an unknown number of arguments, so we'll use the rest operator

const compose = (f, g) => (...args) => /* Magic happens... */

And that returned function needs to apply both f and g (in left-to-right order) to our args, so we'll nest those function calls and spread the args into the first one (g)

const compose =
  (f, g) =>
  (...args) =>
    f(g(...args))

The Result

Now, this code from earlier will work

const addInc = compose(inc, add)

addInc(1, 2) // 4

We can also use it to compose add and dbl

const addDbl = compose(dbl, add)

addDbl(1, 2) // 6
A Note About Function Arity

Function arity, in case you're not familiar with the term, is essentially a fancy word for the number of arguments a function expects.

When composing functions, only the first function to be applied can accept multiple arguments.

After the fist function is applied to our arguments, each function after that is being applied to the result of the previous function. Functions can only return a single value, so that result will only ever be a single value.

This doesn't have to be a limitation. For functions with multiple arguments within a composition, you can use curried functions and partial application to create unary functions to use in your compositions.

Implementing the composeAll Utility

At this point, we can compose two functions with our compose utility. This gave us the ability to create the addInc and addDbl functions by composing add with inc and dbl respectively. How do we handle a composition of all three functions?

Let's say we want an addIncDbl function that adds two numbers, increments the result, and then doubles the result of that. We can accomplish that with our compose utility by using it twice:

const addInc = compose(inc, add)

const addIncDbl = compose(dbl, addInc)

addIncDbl(1, 2) // 8

And we could remove the extra identifier by nesting calls

const addIncDbl = compose(dbl, compose(inc, add))

addIncDbl(1, 2) // 8

These both work, but once we start getting into bigger compositions, this is going to be just as tedious and/or unreadable as our initial examples that didn't have the benefit of a compose utility.

Let's build on what we already have and create a variadic composeAll utility that will take in a variable number of functions as arguments and compose them all.

The Goal

We want something that works like this:

const addIncDbl = composeAll(dbl, inc, add)

addIncDbl(1, 2) // 8

Let's Build It

We know that we want to take in a set of functions of unknown length and return a single function that will apply all the passed functions.

Let's start by taking in our functions. We'll create a function that creates an array of functions using the rest operator

const composeAll = (...fns) => /* Magic happens... */

In our function body, fns will be an array of functions.

Any time we want to take a collection (array) of things and turn that into a single item, you're going to want to reach for reduce.

const composeAll = (...fns) => fns.reduce(/* Magic happens... */)
A Quick Refresher on Reduce

The Array.reduce takes a function (a reducer) that will receive an accumulator (the value you're building up over time) and an item from the array. It'll iterate over the array, building up the accumulator until it runs out of array items. We can also supply a second (optional) argument to reduce, which acts as the initial value of the accumulator. If we don't pass in that initial value, reduce will pass the first two elements in the array to the reducer function on the first iteration.

As luck would have it, we already have a function that takes in two functions and returns a function. It's the compose function we built previously. We can use compose as our reducer without having to make any changes!

Our resulting function will look like this 🤯.

const composeAll = (...fns) => fns.reduce(compose)

The Result

Now we have a composeAll utility that will allow us to compose an arbitrary list of functions. Our target functionality checks out.

const addIncDbl = composeAll(dbl, inc, add)

addIncDbl(1, 2) // 8

Let's throw a couple extra functions in here just to exercise it a bit more thoroughly. We'll add a square utility and then compose it, along with some extra instances of inc and dbl

const square = (a) => a ** 2

const addIncDblSquareDblInc = composeAll(inc, dbl, square, dbl, inc, add)
addIncDblSquareDblInc(1, 2) // 129

And it works! I know this example is quite contrived and a bit silly, but it shows that our composition is working as expected... if you walk through the math with a calculator, you'll end up with the same result.

Next Steps

You might want to follow the pattern for composeAll and just make a single compose function that handles n input functions. Your best bet, would be to install a utility library like Ramda. Ramda comes with compose and many more utilities that are auto-curried, treat data as immutable, and are pretty much purpose built for composition.

If video is your thing and you want to see this played out in video form, you can watch my video on the same topic on egghead.io

And if you want to dive deeper into Ramda, I have a whole playlist on egghead.io about coding with Ramda.

Conclusion

While the example functions are contrived, the utilities and the idea of composing functions can be incredibly powerful. When you can break your problems down into small, focused steps and then assemble those steps in multiple ways, you gain a bit of a superpower in your code.

I've worked in code bases where this was a standard approach and once we hit a level of maturity, new functions were typically just new compositions of existing functions. The code became incredibly declarative and you could read function definitions like sentences that told you exactly what they were going to do with your data.