Improve Composition with the Compose Combinator

Paul Frend
InstructorPaul Frend
Share this video with your friends

Social Share Links

Send Tweet
Published 7 years ago
Updated 5 years ago

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.

Stephen James
Stephen James
~ 6 years ago

The identity function works, because each reducer has only one argument? That is, in the first iteration when accumulator is x=>x then ...args is a single value?

Stephen James
Stephen James
~ 6 years ago

I am confused [1,2,3,4].reduce(isNot2Filter(isEvenFilter(doubleMap(pushReducer))), []) // this was [8]

[1,2,3,4].reduce(compose(isNot2Filter, isEvenFilter, doubleMap)(pushReducer), []) // this is [4, 6, 8]

so they are not the same result? Your compose calls the functions in reverse order to the nested example.

compose(f, g, h)(x) is resulting in h(g(f(x))) not f(g(h(x)))

[1,2,3,4].reduce(compose(doubleMap, isEvenFilter, isNot2Filter)(pushReducer), []) // this is [8]

Paul Frend
Paul Frendinstructor
~ 6 years ago

@stephen you're right to be confused as there's a bug in the compose function, sorry about that. accumulation should be calling fn instead of the reverse. I will update this over the weekend, thanks for pointing it out.

Stephen James
Stephen James
~ 6 years ago

Well on the upside I really understand what it is doing now. :)

Dave Garwacke
Dave Garwacke
~ 6 years ago

I was really scratching my head on that one, thanks @stephen!

Paul Frend
Paul Frendinstructor
~ 6 years ago

For anyone reading this thread - the bug in the code has been fixed in the video.

Greg Jones
Greg Jones
~ 6 years ago

Isn't x => x unused in this case? It seems like each function that receives it is just expecting the single argument of reducer, but we're adding x => x as a second argument.

Paul Frend
Paul Frendinstructor
~ 6 years ago

@greg - the identity function (x => x) is purely there to make sure we don't throw an error if you call compose without any arguments, as you can't call .reduce on an empty array without it. If you call compose with nothing but have the identity function there it will still work. I.e. calling compose()() will be undefined.

If you don't mind that it throws an error - you can remove it.

Edit / update: @greg you're spot on. Sorry was looking at my code samples not the code in the video. A bracket is in the wrong spot so the identity fn is not being passed as the second arg to .reduce.

Correct compose fn:

const compose = (...functions) =>
  functions.reduce((accumulation, fn) =>
    (...args) => accumulation(fn(...args)), x => x);
Tobias Barth
Tobias Barth
~ 6 years ago

I watched the last three videos twice because I was confused by the bug. And then I was confused because it was fixed and I didn't know why it was not confusing me anymore. :) Thanks for the update!

Greg Jones
Greg Jones
~ 6 years ago

@Paul, in the video you're not passing x => x as the initial state to the .reduce, it's being passed on the reducer. So the function should still throw an error if invoked with an empty array. Is x => x the right approach when it doesn't match the signature of the input reducers?

Divyendu
Divyendu
~ 6 years ago

Awesome stuff, quick question. What kind of sorcery are you using to show the value inline like a REPL?

Grzegorz Laszczyk
Grzegorz Laszczyk
~ 6 years ago

@Divyendu I belive it is https://quokkajs.com/

Markdown supported.
Become a member to join the discussionEnroll Today