DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’»

DEV Community πŸ‘©β€πŸ’»πŸ‘¨β€πŸ’» is a community of 963,864 amazing developers

We're a place where coders share, stay up-to-date and grow their careers.

Create account Log in
Cover image for reduce or for…of?
Luke Shiru
Luke Shiru

Posted on • Updated on • Originally published at lukeshiru.dev

reduce or for…of?

Discussion was locked because I moved away from DEV to blog on my personal website. If you want to chat about the content of any article, hit me up in Twitter.


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;
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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);
    }
});
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (7)

The discussion has been locked. New comments can't be added.
Collapse
 
etampro profile image
Edward Tam

Nice article. One thing to note with reduce is that there is a gotcha with async/await. Since async functions always return a promise, using an async operator with reduce will end up with a list of promises, which we will need to resolve manually at the end. For..of on the other hand can simply follow the standard await pattern.

Collapse
 
lukeshiru profile image
Luke Shiru Author

I think I never saw code that goes from a reduce to a Promise (generally I used map, filter or something like that). If you have a list of promises, remember async/await is just syntax sugar, so you can use Promise.all or Promise.allSetled πŸ˜„

Collapse
 
etampro profile image
Edward Tam

Sure. I think map and filter all have the same behavior as well. We can always get around it, it is just a bit tricky sometime.

I think this article explains the behavior quite well.

Collapse
 
the3rdc profile image
Cody Crumrine

Thanks for the post! Great breakdown.

I'm gonna chime in here to be that guy that says "do the thing that makes the most sense semantically".

If what you're doing is best expressed as "working through each item" use "for...of". If it's best expressed as "condensing a series of items down to a single item" use reduce.

Collapse
 
uahnbu profile image
uahnbu

Lol you're doing reduce extremely wrong. Instead you should do

const object = array.reduce(
  (target, key) => (target[key] = null, target),
  {}
);
Enter fullscreen mode Exit fullscreen mode
Collapse
 
lukeshiru profile image
Luke Shiru Author

If you do mutations such as target[key] = null, then why use reduce at all?

Collapse
 
uahnbu profile image
uahnbu • Edited on

So why don't you do

for (key of array) {
  object = { ...object, [key]: null }
}
Enter fullscreen mode Exit fullscreen mode

instead? I prefer reduce because it's a one-liner. And it's not that I did mutation or anything. I just add keys to an object defined inside reduce that's all.
And, if you do it right, reduce is actually faster than for...of in most cases.

Update Your DEV Experience Level:

Settings

Go to your customization settings to nudge your home feed to show content more relevant to your developer experience level. πŸ›