Jump to content

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 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: "loucyx" },
{ 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 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.