Learn Function Composition By Building compose and composeAll Utility Functions
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
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... */)
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.