Improve Composition with the Compose Combinator

Paul Frend
InstructorPaul Frend
Share this video with your friends

Social Share Links

Send Tweet

To make our composition more readable and easier to name we are going to ceate a compose function we can use to avoid having to manually nest our transducer calls.

We'll also go over semantically naming your compose functions for extra readability.

Instructor: [00:00] Instead of calling our functions in this nested fashion, let's create a higher-order function which can do that for us. To do this, we'll need a function that composes other functions together. We can define a function called Compose, which, when given functions as arguments and an initial value to the innermost function, should be the same as calling those functions in that order, so F of G of X.

[00:28] In our example from above, we've got Compose isNot2 filter, isEven filter, doubleMap, with pushReducer as the innermost value, should be the same as isNot2 filter calling isEven filter, calling doubleMap, calling pushReducer. I'll just put them on the same lines so you see that payed off.

[00:53] You might be wondering why we're passing pushReducer as another function call and not as the fourth argument into Compose. That's because we want our Compose function to be a combinator. A combinator is a function which creates a new function with some relationships between the functions you passed in.

[01:09] This new function has some baked-in behavior for how these passed-in functions should interact when we call it again, in our case, calling each other here, from right to left. When we call it again, this is when the functions will get called, but based on this relationship. In functional circles, you might also see this compose function referred to as the B combinator or bluebird.

[01:31] Now let's actually create it. Let's comment this out, and we'll call it Compose. We know we want to take an arbitrary amount of functions, and then let's reduce over those functions. Our reducer will return a function, which calls the accumulation with the next function in line and the arguments. Then we want the identity function as our initial seed.

[01:59] If we step through this, it may seem it's being used with the arguments we used up here. When we call it once, we get an array of functions, which are these three functions. Then we step through all of those functions with our reduce function. What we're doing here is folding each function into another function, which is our accumulation.

[02:20] The first time we go through this, our accumulation is our identity function. The first accumulation that we build up ourselves we'll be calling the accumulation, which will be the identify function, with our first function that we pass the args to.

[02:37] The second time we go through, the function will be isEven filter, which will be passed to our accumulation, which, at this stage, is the identify function calling isNot2 filter. I hope you can see how this is recreating that nested structure. If it's complicated, I recommend setting some break points in here, have a play with it, and just see what happens in each iteration.

[03:02] Now let's use this to compose our transducers. We'll copy our example from up here and we'll paste it in. I'll just put this on separate lines for readability. Now we can replace these nested calls with our call to Compose. Let's call Compose, and we'll just change this to commas instead, like so.

[03:26] Now, if we run this, we get 8 as our result, which is the expected outcome when going through this composition of filtering out the value 2, checking if it's even, and doubling it. What's really useful is that we can also give our composition semantic meaning by naming it.

[03:44] On the line before here, I'm going to call this cleanNumbers transform. That's going to be a call to our Compose function, but only the first call. Then we can use this compose version instead. If we run it, we see we've still 8 as our result.

[04:02] There we have it. We can now compose these transducers together, without having to specify the innermost result-building reducer straightaway. When we do provide it down here, we get a single transformed reducer back, which conforms to our reducer interface.