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 X 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 reduceconst sum = numbers.reduce((total, number) => total + number, 0);
// Using for…oflet 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: "loucyx" }, { type: "bot", username: "bot" },];
// Using reduceconst usersByType = users.reduce( (usersByType, user) => ({ ...usersByType, [user.type]: [...(usersByType[user.type] ?? []), user], }), {},);
// Using for…ofconst 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 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: "loucyx" }, { 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.