Just Enough FP: Composition
Composition is the culmination of all the previous “Just Enough FP” blog posts. It’s where we combine our knowledge of higher order functions, currying, partial application, and pointfree programming into a new concept that can really unlock our functional potential.
Before I get into explaining this concept, I want you to think back to your high school math classes. You might recall somewhere around the time of Algebra 1 or 2 learning about mathematical functions, like this simple one: f(x) = x + 1
. This function takes any x
and always returns x + 1
. You might recognize this as a pure function (all mathematical functions are pure functions).
Now, let’s add a second function: g(x) = x * 3
. This is also a pure function. What if I wanted to take the output of one function and feed it as the input of another function? It would look like this: f(g(x))
. Now, x
will first be mulitplied by 3
, and then 1
will be added to it. If I provide an argument of 4
for x
, the result will be 13
.
This is functional composition.
It is the act of taking the output of one function and passing it directly as the input to the next function. In its most primitive form, it occurs through nesting functions like you see above. Let’s make a practical-ish example of this.
An example I like to use involves manipulating a string through a series of functions. Let’s start by defining those functions.
const scream = str => str.toUpperCase()
const exclaim = str => `${str}!`
const repeat = str => `${str} ${str}`
The scream
function uppercases a string, exclaim
adds an exclamation point, and repeat
, well repeats the string (with a space inbetween). You’ll notice that each function has the same arity. If you’ve forgotten what arity is, check out the currying post before going further. Each one is a curried, unary function. Also, notice that the argument type for each function is the same. They all expect a string
. Our composition wouldn’t work properly if we were mixing types, but I’ll save that discussion for another time. Because all of our functions expect only one more argument and have the same type signature, we can nest them together, passing the output of one as the input of the next without any worries.
repeat(exclaim(scream('Just Enough FP is great')))
// JUST ENOUGH FP IS GREAT! JUST ENOUGH FP IS GREAT!
repeat(exclaim(scream('i luv learning')))
// I LUV LEARNING! I LUV LEARNING!
That’s pretty cool! But imagine you had even more things you wanted to nest, or that your function names were longer than one word. That nesting would get very verbose and difficult to read. Is there a way we can tidy this up? Of course there is.
If you recall from the currying and partial application posts, curried functions do not evaluate completely until they receive their final argument. You’ll also recall that they hold their previously partially applied arguments in closure awaiting that final argument. If we were able to programmatically combine all our functions into one new higher order function that awaited its final argument, we’d have ourselves a cool way of generating compositions.
Well, here’s that cool way. The compose
function:
const compose =
(...fns) =>
x =>
fns.reduceRight((acc, fn) => fn(acc), x)
The compose
function accepts any number of functions as arguments and returns a new function that awaits any value x
. When x
is received, we take our array of functions and call reduceRight
(the same as Array.prototype.reduce
, but we start from the end and work towards the beginning of the array), passing in the accumulated result as the input to the next function, starting with our value x
. compose
can be used like this:
const withExuberance = compose(repeat, exclaim, scream)
withExuberance('this is a-maz-ing')
// THIS IS A-MAZ-ING! THIS IS A-MAZ-ING!
You see, we were able to generate a composition by passing in the functions from before as arguments to the compose
function, which gave us a brand new function awaiting our string. This string was passed first to scream
, then to exclaim
, and then to repeat
.
”Wait! What?!” No, I can’t read your mind, it’s just the response I get every time I teach this.
In our mathematical compositions, the innermost function is always called first. When we nested our functions, they read from right-to-left: repeat(exclaim(scream(x)))
. So compose
is designed to order its functions the same way, from right-to-left (or bottom-to-top if you have so many they don’t fit on one line). This takes some time to get used to, but with a little practice, becomes fairly easy to read.
We can actually break our composition down into smaller compositions and compose them together. Since we only have three functions, I’ll only compose scream
and exclaim
.
const withExcitement = compose(exclaim, scream)
const withExuberance = compose(repeat, withExcitement)
withExuberance('composition is blowing my mind')
// COMPOSITION IS BLOWING MY MIND! COMPOSITION IS BLOWING MY MIND!
You can start to imagine how the ability to make small compositions and use them in larger ones can add some useful functionality to a program or application. So long as the arity and types match up, you should be able to compose at will without fear.
Bonus: pipe
If you ever work in a purely functional language, you’ll likely become very accustomed to using compose
and its right-to-left signature. However, to ease you into functional programming. There is an alternative: the pipe
function.
It’s exactly the same as compose
, but instead of reduceRight
, we use reduce
.
const pipe =
(...fns) =>
x =>
fns.reduce((acc, fn) => fn(acc), x)
Our compositions now read left-to-right, top-to-bottom. Forgive me, I had to teach you composition the mathematical way first. Our pipe
compositions now would look like this:
const withExcitement = pipe(scream, exclaim)
const withExuberance = pipe(withExcitement, repeat)
withExuberance("I can't think of an interesting string")
// I CAN'T THINK OF AN INTERESTING STRING! I CAN'T THINK OF AN INTERESTING STRING!
Conclusion
Composition is the act of passing the result of one function directly as the input of another function. With our knowledge of higher order functions, we are able to generate useful compositions with a compose
or pipe
function that await any data to operate on. It provides a mechanism for building up complex functionality through combining simpler and smaller functions.
In this series, I’m going over material from my Just Enough Functional Programming in JavaScript course on egghead. This post is based on the lesson Build Up Complex Functionality by Composing Simple Functions in JavaScript.