Advanced usage

Sometimes our reductions require multiple steps. Say we have a lemon tree, but want to end up with a box of ripe yellow lemons, labeled by weight, with any underweight lemons removed.

This might require three trips through the array: we first want to reduce our lemon tree down to a bucket of ripe lemons, then we’ll go through that bucket and put a label on each lemon with its weight, and finally we’ll go through the bucket again and throw out all lemons that weigh less than three ounces.

Three passes through our little lemon tree array is not a big deal, but as data becomes larger and reducer functions more computationally intensive, we may start to see performance issues. Ideally, we’d only have to loop through once. We can do this with a transducer.

What’s a transducer?

A transducer is a function that takes a reducer as input and returns another reducer as its output. The reducer it returns may transform the current value or include some filtering logic. We’ll see how we can use this reducer-to-reducer symmetry to our advantage.

Remember our keepRipeLemon reducer from the beginning? There’s something special about it—it already makes use of a reducer-within-a-reducer! Notice that it passes off the instructions for “what to do with ripe lemons” to putInBucket :

At the time, we used putInBucket just because it happened to provide the instructions we needed, but as we’ll soon see, we can use this reducer-within-reducer concept to piece together mega-reducers (not official name).

First, let’s make sure we understand the long-form version where we call reduce one time for each step in the process. We have four total reducers:

putInBucket : Instructions for combining a lemon with a bucket.

: Instructions for combining a lemon with a bucket. keepRipeLemon : Instructions for testing whether a lemon is ripe and keeping it if so.

: Instructions for testing whether a lemon is ripe and keeping it if so. labelLemonWeight : Instructions for weighing each lemon (a random number between 0 and 12). Adds a weight property to our lemon object before passing it off to putInBucket .

: Instructions for weighing each lemon (a random number between 0 and 12). Adds a property to our lemon object before passing it off to . keepHeavyLemon : Instructions for filtering out too-light lemons.

Notice that all of our reducers use putInBucket as the instructions for “what to do next”. Find a yellow lemon? Put it in the bucket. Done weighing and labeling? Put it in the bucket. (Find a green lemon? Give the bucket back without doing anything).

The only thing special about putInBucket is that it provides the specific instructions for combining the lemon and bucket— bucket.push(lemon) . It doesn’t rely on any other outside instructions to get the job done. Let’s call this kind of self-sufficient function a ‘base reducer’.

But remember, there’s more than one way to combine things — we may want to tally our lemons instead of bucketing them. We’ll make our reducers flexible by passing the “what to do next” instructions as an argument instead of hard-coding putInBucket . Since we’re not sure if we’ll be tallying or bucketing lemons, we’ll also rename our bucket accumulation to runningTotal .

We end up with a function that takes nextInstructions as an argument and returns a reducer:

I find ES6 arrow notation easier to read, so from here on out I’ll use arrows instead of the `function` notation above.

In the example above, keepRipeLemon initially expects instructions for what to do next if the lemon passes the ripeness test. When we call this function with those instructions, we are returned a function with the familiar reducer signature: (acc, cur) => newAcc

We can now load up keepRipeLemon with our putInBucket base reducer if we want our next instructions to be to put ripe lemons in our bucket:

const keepRipeLemonsInBucket = keepRipeLemon(putInBucket)

Or, if we’d rather tally up how many ripe lemons we have, we could load it up with our tally reducer instead:

const tallyRipeLemons = keepRipeLemon(tallyLemonColor)

When we reduce over our lemon tree, we get the same result as when we used our hard-coded reducers:

lemonTree.reduce(keepRipeLemonsInBucket, []) // [{ color: 'yellow'}, { color: 'yellow'}, { color: 'yellow' }]

lemonTree.reduce(tallyRipeLemons, { green: 0, yellow: 0 }) // { green: 0, yellow: 3 }

Our initialValue for keepRipeLemonsInBucket is an empty array, and the initialValue for tallyRipeLemons is an empty tally sheet. We have to respect what type of accumulation our base reducer expects— putInBucket expects a bucket array and tallyLemonColor expects a tally sheet object.

We could say that our reducers are becoming composable. Meaning, we can build up the functionality we want with reusable pieces instead of hard coding one-off functionality. Let’s take this idea one step further.

Making a mega-reducer

You may not have realized it, but we just built a transducer. Although the origin of this word is often cited as the combination of “transform” and “reducer”, I like to think of the prefix “trans” in the context of reducers passing through one another.

Let’s modify all of our existing reducers to the new composable form:

Our data will now pass through each of these reducers and will be processed by the next reducer in the chain before being returned. Our nextInstructions could be a base reducer if we want to update the accumulator right away, OR our nextInstructions could be another one of these new composable transducers. Both take a runningTotal and a lemon as input, and both return a new accumulator, so we don’t have to differentiate between the two.

We can now build up arbitrarily long chains of instructions as long as a base reducer gets called at the end of the chain!

Keep in mind that data is not being reduced at this step. We are simply building up a single set of instructions that will be able to perform the reduction when the time comes.

Let’s reduce now:

Great! We can now compose together transducer functions. We originally wanted “a box of ripe yellow lemons, labeled by weight, with any underweight lemons removed.” Let’s make that happen.

Each new reducer gets its nextInstructions passed in when it is created:

We can build up our reducer in separate steps (top example), or create it all in one go (bottom)

Done! We have now reduced our lemon tree down to a box of ripe lemons, labeled by weight, with underweight lemons removed. And with only one trip through the lemonTree array!

One level deeper: a generic ‘filter’

You may have noticed that keepRipeLemon and keepHeavyLemon follow the same basic pattern. We return the result of calling nextInstructions only if the current lemon passes some test. Otherwise, we just return the current running total without modifying it. This is a pretty good sign that we can extract out that test as a separate step and make our composable-reducer-creator even more generic and useful:

This is getting pretty abstract. In fact, I still have trouble visualizing the exact path through the reducer from here (which is part of the reason I’m writing this post!). Let’s take a moment to trace through what’s happening:

Our filterInstructions first expects a tester function called test that returns true or false based on the lemon we pass to it. If test(lemon) returns a truthy value, we’ll set newRunningTotal to be the result from the next step in the instructions. If test(lemon) is falsy, newRunningTotal will remain the current runningTotal .

Returning the current runningTotal without calling nextInstructions is the equivalent of saying “here’s your bucket back” or “here’s your tally sheet back” without altering it.

“I only keep yellow lemons, but you gave me a green one, so here’s your empty bucket back”

When we pass in our test function to filterInstructions , we are returned a function that takes a nextInstructions reducer as an argument, which will either be a base reducer or another one of our custom reducers. Once we provide nextInstructions , we are finally returned a function that has the standard reducer signature.

This function is now “loaded up” with the ability test a currentValue , and to hand off further processing to the nextInstructions if the test passes.

A generic ‘map’

We can do a similar process to extract the “weighing and labeling” logic from our weighLemon reducer. Transforming a value by passing it through some function and capturing the result is called “mapping”, so we’ll call this function mapInstructions :

Again, let’s step through. Our mapInstructions function first expects a mapperFn . We’ll create a mapper function that transforms a lemon into a lemon-with-weight-label. Once we pass it our mapperFn another function is returned, which expects some nextInstructions as its argument. We’ll use our trusty old putInBucket . Finally, we’re ready to reduce.

Now that we have these final transducers, we can compose to our hearts content. Here’s what our final version looks like:

Same result: ripe, heavy, labeled lemons in a bucket. But much more reusable!

Reducing Redux state

Before we part, let’s take a quick look at Redux, since it’s likely the place we’ll be encountering reducers in action. Manipulating state in a Redux reducer is no different than reducing over our lemon tree. We provide instructions for combining an accumulator (the state of our redux store), and a current action (an object with a type property). The value we return will become the new state of the store, and will be used as the accumulator the next time the reducer is called.

Redux reducers often use a switch statement to determine which instructions to follow:

I won’t go into detail here since we’ve already covered so much ground. But other than a few implementation details, it should look pretty familiar!

Summary

Reducers run the gamut from “dead simple” to “brain-numbingly hard to visualize”, especially once composition enters the picture. Try to remember that a reducer is nothing more than instructions for combining two things: a running total, and a current value.

And also, how on earth did I go this whole post without reducing my lemon tree into lemonade?? Show me how you’d do that in the comments!

Notes: