Making reducers more intuitive

I wanted to make this post because, while I’ve been able to use reducers comfortably for a while, I got a certain realisation about them a while back. That realisation made them much more intuitive and easier for me to use.

Before, I knew what reducers did and how they worked. But, after, I understood them much more intuitively and more holistically with regards to where they fit in with everything else. This post is here in case it can do the same for you.

Notes before we start

As a prerequisite, I assume that you already know the basics of using reducers with the reduce array method. This post won’t explain the basics of using them.

For example, you should be able to understand the following code, even if it’s a bit difficult:

const reducer = (resultSoFar, currentValue) => resultSoFar + currentValue;
const result = [1, 2, 3, 4, 5].reduce(reducer, 0); // 15

If you prefer, here’s the same reducer defined inline, inside the .reduce call.

const result = [1, 2, 3, 4, 5].reduce((resultSoFar, currentValue) => {
  return resultSoFar + currentValue;
}, 0);

Also, you should understand that the second argument of the .reduce call is the initial value. It’s the value that resultSoFar takes the first time the reducer is called. It’s not mentioned again in the text below.

Finally, this post often mentions that reducers take two arguments. They can take more. For example, the array reduce method passes 4 arguments to reducers. However, this post ignores that detail for the sake of the explanation.

Alright, let’s move on to the explanation.

Reducers combine two things into one

Personally, I think the name "combiner" is a bit more intuitive than "reducer".

That’s what a "reducer" does. It takes two arguments (or more) and combines them into one thing. Then, it returns that one thing.

The first argument is the result so far. It’s often referred to as "accumulator", "previousResult", or even "resultSoFar".

The second argument is the current value of the array.

A reducer takes the resultSoFar and the current value of the array as arguments. It does some sort of operation with them, effectively combining them into one thing. It returns that one thing which becomes the new resultSoFar the next time the reducer is called. Repeat for every element of the array.

Arguments of the same type

Usually, the two arguments are of the same type. For example:

function addReducer(resultSoFar, currentValue) {
  return resultSoFar + currentValue;
}
const result = [1, 2, 3, 4, 5].reduce(addReducer, 0); // 15

function multiplyReducer(resultSoFar, currentValue) {
  return resultSoFar * currentValue;
}
const result2 = [1, 2, 3, 4, 5].reduce(multiplyReducer, 0); // 120

A quick sidenote on naming: Reducers are just normal functions that accept two arguments and return one thing. Generally, I prefer to name them something "normal", treating them like any normal function in the codebase, rather than specifically identifying them as reducers.

For example, the reducers in the example above can be named add and multiply respectively.

function add(a, b) { // equivalent to addReducer above
  return a + b;
}
function multiply(a, b) { // equivalent to multiplyReducer above
  return a * b;
}

Combining two things of different types

The two things a reducer combines don’t have to be of the same type. A reducer won’t always combine two numbers into one number, or two strings into one string.

It can also combine things of different types. For example, it can combine an object with a number, or a string and a function into a string.

All that matters is that it takes two arguments, does some sort of operation, and returns one thing.

Here’s an example where a reducer takes an object and a letter. It adds the letter into the object. Then, it returns the object.

const addLetterToObject = (obj, letter) => {
  obj[letter] = letter;
  return obj;
}
const letters = ['a', 'b', 'c'];
const result = letters.reduce(addLetterToObject, {}); // {a: 'a', b: 'b', c: 'c'}

Each time, the function addLetterToObject is called with the result so far (obj) and the current value of the array (letter). It combines them into an object which it returns and which becomes the new result so far.

Sidenote: addLetterToObject above is an impure function. Usually, reducers are pure functions which would create a new object with the correct results instead of mutating the old one. The details and considerations for this are left for a different post.

Next, here’s a more difficult example using an array of functions.

The scenario is that we have a long string of text. Something like a blog post. We want to operate on it in some way. For example, there may be certain phrases we want to search / replace. Many of them.

Essentially, we’ll have many functions of the type:

const replaceHelloWithGoodbye = (str) => str.replace(/hello/g, 'goodbye');
const censorSwearword = (str) => str.replace(/swearword1/g, '******');
const replaceYesWithNo = (str) => str.replace('Yes', 'No');

We want to run all of them on the string. Each time, the resulting string will be passed onto the next function to run. That’s what the array reduce method does, so let’s use it.

Here’s how we might do it:

// Our replace functions
const replaceHelloWithGoodbye = (str) => str.replace(/hello/g, 'goodbye');
const censorSwearword = (str) => str.replace(/swearword1/g, '******');
const replaceYesWithNo = (str) => str.replace('Yes', 'No');

// Our reducer
const replaceString = (str, replaceFn) => replaceFn(str);

const message = 'Hello, this is a blog post. Yes, blah blah swearword1 blah';
const newMessage = [
  replaceHelloWithGoodbye,
  censorSwearword,
  replaceYesWithNo
].reduce(replaceString, message); // 'Hello, this is a blog post. No, blah blah ****** blah'

The reducer replaceString takes a string str and a function replaceFn. It calls replaceFn(str), which results in a string. It returns the result. Effectively, our reducer has combined the two arguments into one thing, which it returned. That becomes the new accumulator / resultSoFar for next time.

It may seem more complicated because we’re calling .reduce on an array of functions. It takes some mental gymnastics to understand everything. Each value of the array (a function) is passed to the reducer as an argument. The reducer does some sort of operation with its two arguments (in this case, it calls replaceFn(str) and then returns the result.

It may look weird, but it’s pretty useful.

Sidenote: all replaceString is doing is calling the second argument with the first argument. A more appropriate name for it may be call or callRight (since it’s calling its second argument, the argument on the right).

When to use reducers with the array reduce method

Sometimes, using .reduce can make the code more concise and easier to read.

For example, here’s the code for summing a list of numbers:

const numbers = [1, 2, 3, 4, 5];
const add = (a, b) => a + b;
const result = numbers.reduce(add, 0);

Here’s the equivalent code with a for-of-loop:

const numbers = [1, 2, 3, 4, 5];
let result = 0;
for (const number of numbers) {
  result += number;
}

After you’re comfortable with reducers and the array reduce method, the example using reduce is arguably cleaner and easier to understand.

I encourage you to practice using reducers until you get to that stage. But, in the meantime, I recommend being pragmatic. Use the option that’s easiest for you (and your team) to understand and work with. Don’t automatically use reducers for everything because some people claim it’s universally better.

Reducers in different contexts

Reducers in different contexts work the same way. For example, in React and Redux. The same concepts apply. They accept a particular number of arguments. They do some operation to effectively "combine" the arguments. Then, they return one thing.

Final notes

As a final note, the name "reducer" isn’t too bad. Reducers take a number of arguments and they "reduce" them into one thing. Particularly in the context of the array .reduce method, they eventually "reduce" an entire array into one thing. But I still believe that the name "combiner" is more intuitive, so that’s why I pointed it out.

Anyway, that’s it for this article. I hope that you found it useful.

As always, if any points were missed, or if you disagree with anything, or have any comments or feedback then please leave a comment below.

For the next steps, I recommend looking at other articles about functional programming.

Alright, thanks and see you next time.

Image credits

Featured image: Photo by Los Muertos Crew from Pexels
Coffee beans on measuring spoon: Photo by cottonbro from Pexels

Subscribe
Notify of
guest
0 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments