reduce or for…of?
The claim
Let’s start with a “bold claim” made by Jake
Archibald about using Array.prototype.reduce
:
All code using
Array.prototype.reduce
should be rewritten without it so it’s readable by humans.
This claim started a great discussion on Twitter and inspired me to summarize what I think in this article.
The readability problem
The problem with Array.prototype.reduce
is mainly with readability, though
some developers like myself generally prefer the functional approach over
control structures such as for…of
. So let’s see an example using both methods:
const numbers = [1, 2, 3, 4, 5];
// Using reduce
const sum = numbers.reduce((total, number) => total + number, 0);
// Using for…of
let sum = 0;
for (const number of numbers) {
sum += number;
}
I’m cheating a little bit with this example because some of the few scenarios in
which Array.prototype.reduce
is more readable are sums and products. Let’s try
another example, this time with a more complex operation:
const users = [
{ type: "human", username: "lukeshiru" },
{ type: "bot", username: "bot" },
];
// Using reduce
const usersByType = users.reduce(
(usersByType, user) => ({
...usersByType,
[user.type]: [...(usersByType[user.type] ?? []), user],
}),
{},
);
// Using for…of
const usersByType = {};
for (const user of users) {
if (!usersByType[user.type]) {
usersByType[user.type] = [];
}
usersByType[user.type].push(user);
}
// Soon we will be able to use `Array.prototype.group` for this 😊
const usersByType = users.group(user => user.type);
Array.prototype.reduce
isn’t always the most readable, and even if we didn’t
cared about readability, we also have to consider that the performance is
worsened in this case because, to keep it immutable, we are creating a new
object in every iteration. One other limitation of Array.prototype.reduce
is
that it only works with arrays, while for…of
works with any iterable.
A functional for…of
My suggestion to keep the syntax “functional”, while gaining the benefits of
for…of
is to create a simple function that uses for…of
under the hood, but
which external interface is similar to Array.prototype.reduce
:
const reduce = (iterable, reducer, initialValue) => {
let accumulator = initialValue;
for (const value of iterable) {
accumulator = reducer(accumulator, value);
}
return accumulator;
};
const numbers = [1, 2, 3, 4, 5];
const sum = reduce(numbers, (total, number) => total + number, 0);
I use this approach with my library @vangware/iterables, which includes a reduce function that works with synchronous and asynchronous iterables.
A structured reduce
We could also use libraries such as immer to go the other way around and write code that has “mutation ergonomics” without the mutations:
import { produce } from "immer";
const users = [
{ type: "human", username: "lukeshiru" },
{ type: "bot", username: "bot" },
];
const usersByType = produce({}, draft => {
for (const user of users) {
if (!draft[user.type]) {
draft[user.type] = [];
}
draft[user.type].push(user);
}
});
Conclusion
JavaScript is a multi-paradigm language, and as such, it gives developers multiple ways to solve the same problem. The important thing is to be aware of the pros and cons of the approach we choose. I prefer the functional approach, but I’m well aware of the limitations that come with it, though from my point of view, the benefits outweigh those limitations.