In our previous lesson, we created map()
and filter()
functions based on reduce, but we still couldn’t compose them together.
In this lesson, we’ll solve this by making these functions into transducers. This will let us compose the reducing behavior of map()
and filter()
, without coupling this composed transform to a data type.
[00:00] The first obstruction we want to do is a decomposition, where we want to remove the dependency on this array. We want this whole map function to be a reducer we can post to reduce. Instead of returning the result of this whole reduction, we just want to return the actual reducer.
[00:17] To do that, let's just remove our array and return this reducer. Now let's do the same for filter. I'll copy in our old filter function, and then we'll just get rid of this array again.
[00:33] Now our functions have become decorators for our reducers. That is, you call them once with some argument, and then what you get back is a reducer. We control the behavior of this reducer with this argument that we supplied the first time we called the function. Now we can pass these functions as our reducers, instead.
[00:53] Let's create an array with numbers. On this array, we're going to call reduce, and let's reduce with our filter function, with evenOnly as our predicate, and empty array as our seed. Let's send another call to reduce, where we'll call our map with double the number and an empty array as our seed again.
[01:17] We've got our expected result, where we've only kept our even numbers, and then we've doubled them, but our aim here is to compose filter and map together, and only call reduce once. Let's see if we can solve that by hard-coding the map operation into the filter function.
[01:34] Let's create a new function called Filter that doubles. I'm just going to copy our old filter function and rename it. Instead of pushing onto our array in here, let's replace that will a call to map, and then we'll just take our accumulation and our value. We've got an inner reducer here with our hard-coded map call and this outer reducer, which is the returned function.
[02:05] Now let's run this on an array of values. We'll reduce over those and call a filter that doubles with our evenOnly predicate and an empty seed. We can see we once again get our expected result.
[02:21] Now we're iterating once through the collection and we're both filtering and mapping. But we obviously don't want to hard-code in this map logic in our filter function. Let's parameterize that instead. Let's comment out our old filter function and our example, and let's work with this guy. We'll rename this back to filter.
[02:41] We know we want to parameterize this map call, and we also know that it's a reducer. Let's still take our predicate as our first argument, and just add another level of carrying here, where we'll take our inner reducer. Then, instead of calling map, we'll just call our reducer.
[02:58] Let's copy down our example, put it down here. In order to achieve the same thing as before, we can take our call to map and pass that as the argument to the reducer that the filter call expects. If we have a look at this result, we still get our expected outcome. Let's just get rid of these brackets to make it consistent.
[03:23] Let's talk through this. Our filter function now takes a predicate, which determines the logic for how you want to filter. It then takes the inner reducer, which decides how the value should be built up. Once you've called this filter function twice, you're left with the reducer that you've been able to customize with both a filtering logic and the inner reducer that decides how the values should interact with the accumulation.
[03:49] From this level here, this function that's taking a reducer as an argument and returning and reducer is our first transducer. It's a function that encapsulates some reducing behavior -- in our case it's the filtering logic -- that lets the function consumer decide how the results should be built up by being able to supply this inner reducer.
[04:11] Now let's see if we can compose it. I'll get rid of this example, and we're going to create a few filter variations. Since we carried our function, we get the opportunity to give these filter functions some meaningful names.
[04:26] Let's call the first one isEven filter. That will be a call to filter with our evenOnly predicate. Let's create another one called isNot2 filter. That's going to remove any value that isn't the number two. Let's also define a mapping reducer, which doubles our values. We can call that doubleMap, which will call a map with double the number.
[04:55] Let's put this guys to use. Let's call reduce with isEven filter, which we'll pass doubleMap into. We're still getting a 4 and 8 as our result. Let's also add in our isNot2 filter, and we now only end up with a value 8. Our composition is producing the values we expect.
[05:22] Now let's fix up our map function the same way we did for filter. I'll comment out our old one, and we'll paste it in here. We'll do the same thing. I'll remove these brackets, add in our inner reducer, and instead of manually calling push on our accumulation, we'll call our reducer.
[05:42] Now we've got a map transducer, as well. The problem now is that our map call expects this inner reducer as an argument, and we're calling it down here without an argument. We need to create one more reducer, which will be the innermost reducer in this composition.
[06:01] We can call that pushReducer, and that will take an accumulation and a value. It's going to push the value onto the accumulation, and then return our accumulation. Then we can pass that into our argument into doubleMap. It looks like we forgot to pass in our accumulation. Let's check up here. Yeah, we're just passing our value. Let's see how this went. We've still got our expected result.
[06:36] Finally, we can compose filter and mapping functions together, while only iterating through our collections once. This works without a problem, just because of how our reducer composition works. It works because our transducers take a reducer, but then return another reducer.
[06:54] As we learned when we did composition, a function which returns the same type as output as it takes as input will compose naturally. If we retrace our steps here, pushReducer gets passed as the inner reducer to doubleMap, but the call to doubleMap returns the reducer, which in itself becomes the inner reducer to isEven filter, which returns the reducer, which becomes the inner reducer to isNot2 filter.
[07:20] Our transducers are nothing more than functions that decorate reducers in different ways. They enable natural composition, since they all return reducers with the same signature.
Really awesome, thanks for this great content
great content, Thanks!
BTW, watching this video made my bought Quokka extension... ;)
Thanks for the kind words guys! @hao yeah Quokka is pretty awesome. Bought the pro version myself as well.
May i know where is the source code for this courses? thanks
Maybe I don't see the reason to why create all those abstractions, but wouldn't:
const isEven = number => number % 2 === 0;
const double = number => number * 2;
const filterEvensAndDouble = (acc, number) =>
isEven(number) ? [...acc, double(number)] : acc;
do the trick?
This is where I get lost. To many returned functions. Is there a chance to understand that?
@grzegorz Easy to get confused, there's a few levels of higher order functions going on. Is there a specific point you get lost at? It's a bit hard to help without a bit more context.
@jakob sorry for the late reply. Missed your question. What you've done works fine. The only problem is you're baking in logic for how to filter and map into your function. Lets say you wanted another function that filters based on some other logic that isn't isEven, you'd then have to rewrite the logic for how to filter again in that function. It's likely the logic for how to filter is what you'll be reusing more than the business logic for what to filter by, which is why we want functions that abstract away how to map and how to filter etc.
@paul, I can understand without problems to the point we creating map and filter as reducers. Problems start when reducer
is added. It looks incredibly logic and great but is just mind blowing when I'm trying to remember how to construct and call this functions (like in 03:14 of this lesson).
And why do we need this pushReducer
?
But overall great content so far. Thank you.
@grzegorz thanks for clarifying and sorry for the late reply. Ok, so at 3.14 we’ve modified filter to be a transducer, or you can think of it as a function we can use to create a reducer with specific behaviour. The first time we call it we set the transform behaviour, and the second time we set the reducer behaviour. So when we’ve called it twice, we’ve “pre-filled” all the behaviour we care about, and we’re left with a reducer that will operate according to the logic we’ve set in our first two calls.
The only thing that’s decided before you call filter it is that it will filter based on some logic it doesn’t know about. if this logic returns true, then we will return the result of calling reducer, otherwise we’ll just return the accumulation, thus skipping the value. once you’ve called filter once, you’ve decided what logic the filtering should be based on, in this case the evenOnly predicate. The function now expects a reducer so that it knows how to handle the logic for building up the accumulation. And what we’re doing on that line is passing the result of map(doubleTheNumber) to be the reducer that handles all the values that pass the evenOnly predicate. At this time, map doesn’t need a reducer itself, we’ve hardcoded in how the value should be built up (by calling push).
But when we modify the map function at 5:42, it no longer knows how to build up the value. That’s where the push reducer comes in. We’re basically adding back the logic that was in the map reducer initially, but it comes through the reducer argument. This means we can use map with different logic for building up the accumulation. For instance you can call it with a function that uses accumulation.concat() instead of push, or as you see in later lessons, with different data structures than arrays.
Hope that helps.
I'm curious why the composition
Not sure if I can edit / delete the premature post above but I'm curious why the composition in
[1,2,3,4].reduce(isNot2Filter(isEvenFilter(doubleMap(pushReducer))), []); /*?*/ [ 8 ]
operates left to right? My understanding is function compositions operate from innermost function to outermost function. Thank you -- really learning from this course.
hey @Jesse, sorry for the really late reply. Missed this over the Christmas period :).
You're right in that functions are evaluated from innermost to outermost.
However, we're passing the result of calling isNot2Filter
with a reducer into .reduce
. This function is filter
with both the predicate and reducer applied. So if you look at filter
, you see that it will evaluate the predicate before calling the reducer.
In detail - If you look at isNot2Filter
, it is the result of calling filter
with a predicate for filtering out the number two. So isNot2Filter
is filter
but with the predicate already determined, and it now expects a reducer. However - when you call it again with a reducer - it returns a wrapping reducer that will check against the predicate first - before calling the passed reducer.
So in short - the composition evaluates from innermost to outermost - but those calls just create wrapping reducers that will call other reducers. isNot2Filter
gets called last, meaning the function it creates will be called first when .reduce
calls it.
Hope that helps.
Great stuff. The content on egghead is just miles above anything else out there. Thank you Paul!