Function composition allows us to build up powerful functions from smaller, more focused functions. In this lesson we'll demystify how function composition works by building our own
I'm starting with a handful of functions defined and some specs to confirm that they're working as expected. I've defined simple add, increment, and double functions, and then I have addDouble, which takes in two numbers, uses add to get the sum, and then passes the result of that double to the end result. I'm going to collapse some of these specs here to get them out of the way.
We know that add, increment, and double each work on their own, and we know that addDouble is currently giving us the expected result. I'd like to redefine addDouble as a function composition. So I'm going to come up here and I'm going to take our existing addDouble and comment that out, and I'm going to redefine it.
This time it's going to be a call to compose which we're going to build in a second, and compose is going to take or functions that we're using to get the result as arguments.
We're doing to use double, and we're going to use add, and you'll notice that our functions are in right to left order, in the order that they're going to be run. Our arguments are going to start on the right-hand side, they're going to get passed to the left, until we get a result out the other end.
Let's define compose. Right above addDouble, I'm going to define a constant, I'm going to call it compose. We know a couple of things just by looking at the implementation that we hope to get to.
The first thing is that compose is taking two arguments, both of which are functions. I'm going to accept two arguments, and I'm going to call those f and g. The other thing we know is addDouble is a function. We're calling it down here with two arguments, so that means this needs to return a function, and that function needs arguments, because that's where our numbers are going to get passed in, so I'm going to accept arguments, and then I need to define the function on the right-hand side.
If we look at what happened here, add, which is our first argument here, was called with our arguments. In this case, g is taking the place of add. I'm going to call g with my arguments, and then the result of that is being called by double, which is f, so we can just wrap this whole call with f, and that should be giving us a result.
You can see that our spec is still failing, the problem here is that args is really a single value, so I'm going to use the rest operator to take whatever is passed in here and wrap them up as an array.
Then on this side, I'm going to use the same three dots, this time as the spread operator to pass those individual values in as arguments, and you'll see now our spec is passing.
Now we have a reusable composed utility. To test that out, I'm going to add another spec, and I'm just going to paste some code in here. This describes a doubleInc function, which is going to take in a number, double it, and then increment the result of doubling it. I'm going to define that using the new compose utility.
I'll define const = doubleInc, and I'm going to set that to equal a call to compose, and compose is going to increment after it doubles, so we can pass it its two functions, and everything is working as expected. Now I know I can reliably compose two functions together, but I'd like to be able to compose more.
I'm going to clean this up a little bit, and I'm going to come down here and collapse this doubleInc spec, and I'm going to add one more and just paste in a spec for a function I'm going to call addIncDouble.
This is going to add two numbers, then increment the result of that, and then double result of that. In order to define that, I need to be able to compose three functions together.
I'm going to define a constant and I'll call it addIncDouble, and this is going to be a composition, and the last function of this composition is double, so that will go first. Then I need two more functions here. While compose takes a function as its second argument, compose also returns a function, so I can use a compose in my compose.
I can compose inc and add, and that does exactly what we need it to do, though it's not the easiest code to read. What I'd really like is a version of compose that would allow me to pass it a variable number of functions, and have my data flow through that entire series of functions and spit out a result on the other end.
Let's call that function composeAll, and what I'm going to do is redefine addIncDouble using composeAll which we'll build in a second.
ComposeAll would essentially remove the need for this inner compose, and it would just take all of my functions which would accept my arguments, pass the data from right to left, and give me a result on the other side.
Define a constant composeAll, then we need to figure out how we're going to take our two-argument compose function, and turn that into something that allows a variable number of arguments. I'm going to start by using that spread operator again to accept a list of functions.
That will take these three functions, and if I were to add a fourth and a fifth, those would be included as well, and it's going to give me an array called fns that I can use inside of my function.
Now I have an array of functions, and I want to get one function back. Anytime I have an array of things and I want to get one thing back, I can reach for reduce. I'm going to use fns.reduce, and I'm going to pass that a reducer function.
The typical signature for a reducer function is that it takes an accumulator and one of the items, in this case we'll call f because we know it's going to be a function, and it's going to return an updated accumulator that includes whatever we need from that item.
In our case, we're doing a composition, and we know that both these items are going to be functions, so we can compose an accumulator with f.
We'll see that we already have a passing spec, and the other thing you might notice is that we're taking these arguments directly from our reducer, and passing them into compose, which means we don't actually need to wrap this in an extra function, and we can cut this all the way down to just fns.reduce, passing in compose as a reducer. Now we have nice, reusable compose and composeAll functions that we can use to combine simple functions into more complex functions.