Reduce: JavaScript’s hammer. Sometimes everything IS a nail
Wouldn’t it be nice to have a tool that can solve 95% of your problems? Well you kind of do, and it is a function called reduce. We’ll dive into how and why in this article.
But, what exactly does reduce do?
reduce transforms an array into a single value.
This is a general definition that we are later going to stretch, but for now some examples would help. Here are some things we can do using reduce:
[1,2,3,4] -> 9 // Get the sum of the elements
[1,2,3,4] -> 4 // Find the maximum of the elements
[1,2,3,4] -> '1,2,3,4' // Convert an array to a string
[1,2,3,4] -> [1,3] // Keep only odd numbers
[1,2,3,4] -> [1,4,9,16] // Multiply every element by itself
[1,2,3,4] -> { odd: [1,3], even: [2,4] } // Group elements
We are going to learn how to do these and more in the next few paragraphs.
How does reduce work
Let’s use reduce to sum an array and explain it step by step:
let reducer = (a, b) => a + b;
let result = [1,2,3,4].reduce(reducer, 0);
console.log(result); // Will print 10
reducer is a normal function that, in its basic form, takes two values and returns a new one.
reduce takes this reducer as a parameter and an “initial value” and does the following:
- It calls reducer passing as arguments the “initial value” plus the first element of the array. reducer returns a new value.
- It calls reducer again passing as arguments this new value plus the second element of the array. reducer returns a new value once more.
- This repeats for the third, fourth, etc. elements until all elements of the array are summed up.
Internally, the above example works like this:
let reducer = (a, b) => a + b;
let initialValue = 0;
let accumulator;
accumulator = reducer(initialValue, 1);
accumulator = reducer(accumulator, 2);
accumulator = reducer(accumulator, 3);
accumulator = reducer(accumulator, 4);
console.log(accumulator);
The first parameter is typically called accumulator because it holds the accumulation of previous calculations until that moment.
Since reducer just takes two arguments and returns a new value, there is no limit to what we can do. Let’s try another example. Let’s try to find the maximum of an array:
let reducer = (a, b) => a > b ? a : b;
let result = [1,2,3,4].reduce(reducer, -Infinity);
console.log(result); // Will print 4
Internally, the above example works like this:
let reducer = (a, b) => a > b ? a : b;
let initialValue = -Infinity;
let accumulator;
accumulator = reducer(initialValue, 1);
accumulator = reducer(accumulator, 2);
accumulator = reducer(accumulator, 3);
accumulator = reducer(accumulator, 4);
console.log(accumulator);
As you can see, the internal implementation does not change. Only reducer and initialValue have different values and that’s the great thing about reduce. By manipulating the reducer function, we can get almost any possible value we can imagine.
Note on -Infinity. Since we are trying to find the maximum, setting the initial value to something that is smaller than any other value allows the code to function properly.
More Examples
Now is time for some more tricky examples. What if we wanted to get a string representation of an array? Once more we only need to care about figuring out the reducer and the initialValue:
let reducer = (a, b) => a ? a + ',' + b : b;
let result = [1,2,3,4].reduce(reducer, '');
console.log(result); // Will print 1,2,3,4
In this case our reducer is a bit more complex on the surface but what it does is actually pretty simple. The only reason the ternary operator is there is so that in the first call to reducer we do not add a , before the first number and end up with a result of ,1,2,3,4 .
What if we wanted to find the sum of the property value of an array of objects? Still, just manipulating reducer will do:
let reducer = (a, b) => ({ value: a.value + b.value });
let result = [{ value: 1 }, { value: 2 }, { value: 3 }].reduce(reducer, { value: 0 });
console.log(result); // Will print { value: 6 }
Stretching the definition of reduce
Returning a single value does not mean it has to be a primitive one. We can return an array or an object as well.
For example, returning only the odd numbers in an array would look like this:
let reducer = (a, b) => b % 2 === 0 ? a : a.concat(b);
let result = [1,2,3,4].reduce(reducer, []);
console.log(result); // will print [1,3]
In this example our accumulator is a list and in every iteration we either have an odd number and add it to the list, or return the list as is. Remember, reducer has to return a value, so array.push() would not work.
Another interesting usage is manipulating each element of the array and returning a new array with the manipulated values (this is exactly what the map function of an array does). Let’s create an array with the elements of the original array raised to the power of 2:
let reducer = (a, b) => a.concat(b ** 2);
let result = [1,2,3,4].reduce(reducer, []);
console.log(result); // Will print [1,4,9,16]
Finally, let’s create an object that groups odd and even numbers:
let reducer = (a, b) => b % 2 === 0 ? { even: a.even.concat(b), odd: a.odd } : { even: a.even, odd: a.odd.concat(b) };
let result = [1,2,3,4].reduce(reducer, { even: [], odd: [] });
console.log(result); // Will print { even: [2,4], odd: [1,3] }
Extra Parameters of the Reducer
The reducer function can also accept two more arguments:
- index the position of the element in the array
- array the whole array being reduced
These can be useful in some occasions, like wanting to keep only every second element of the array:
let reducer = (a, b, index) => index % 2 === 0 ? a.concat(b) : a;
let result = [1,2,3,4,5,6].reduce(reducer, []);
console.log(result); // Will print [1,3,5]
Using the array parameter of the reducer is not something that will come up very often but, since reduce is so versatile, it is there if you need it.
Sidenote
You can actually use reduce without specifying an initialValue. In that case the first element of the array will be used as an initalValue. This is not recommended because it can break your code if you try to reduce an empty array. If you are guaranteed that your array will have at least one element, skipping the initialValue can some times simplify your code. For example, converting an array to a string this way becomes much simpler than we did above:
let reducer = (a, b) => a + ',' + b;
let result = [1,2,3,4].reduce(reducer);
console.log(result); // Will print 1,2,3,4
Summary
reduce allows us to abstract away looping through an array and solve a problem by focusing on just two simple things. A function that gets two arguments and returns a value, and an initial value. If you can break a problem to these two parts you have a robust solution that is easy to reason about and maintain.
To reiterate:
Anytime you have an array and you want to get a value based on its elements, your first thought should be to use reduce. It can make your code safer, easier to understand, more testable and you a happy, care-free developer.