DEV Community

Is reduce() bad?

Jasterix on January 20, 2020

A few months ago, I wrote this blog post on the wonders of the JavaScript .reduce() method. In that post, I celebrated my newfound love for the pow...
Collapse
 
vonheikemen profile image
Heiker

reduce is not bad, it's just unfamiliar. In the javascript community for loops are so common and taught so early on that is often seen as simple and easy. People like easy so they will fight you if you try to take that away from them.

Collapse
 
kenbellows profile image
Ken Bellows

Gotta disagree here. In my experience, especially over the last several years, array methods have been pushed super hard in the JS community. I rarely see a simple for loop (of any flavor) these days; it's always long chains of .map().filter().map().reduce().flatMap()... Simple for loops are often criticized with a "why didn't you just use Array.{whatever}?".

Not that this is bad on the whole, I often find this much easier to read than a bunch of for loops. But that's the point that Jake makes in the video referenced in this article, and I find his examples pretty convincing: among all the array methods, reduce is by far the least readable, and it often requires time and mental gymnastics to figure out what a reduce function is doing, where a simple for loop would have been trivial to follow.

Collapse
 
vonheikemen profile image
Heiker • Edited

Gotta disagree here. In my experience, especially over the last several years, array methods have been pushed super hard in the JS community.

Sure the amount of content about map, filter and reduce has increased over the years but that is something you learn later down the road. I would imagine that for and while loops are still the prefered way to teach beginners about iteration.

I rarely see a simple for loop (of any flavor) these days; it's always long chains of .map().filter().map().reduce().flatMap()...

To be fair map is actually quite useful.

that's the point that Jake makes in the video referenced in this article, and I find his examples pretty convincing: among all the array methods, reduce is by far the least readable, and it often requires time and mental gymnastics to figure out what a reduce function is doing,

I like that they show cases where reduce is completely unnecesary, but that doesn't make reduce bad it's just easier to misuse. In some cases the thing that hurt readability is the inline callback with clever code trying to do everything in one expression.

Collapse
 
stackundertow profile image
StackUndertow

Late to the party, but I wanted to chime in.

Maybe I'm a gymnast, but reduce functions don't really seem all that hard to grok since they always do the same thing mechanically:

reduce((seed, next) => {
// evaluate next
// update seed or data type of seed
// return seed or data type of seed
});

Given that, any readability argument comes down to familiarity. If you've never seen a for loop (some languages lack them, ex: Haskell), you may wonder what arcane nonsense for(initializer, evaluator, incrementor){ //operation } is and why the initializer value is available in the context of the operation since the primary tool in your belt is recursion.

I would argue that the worst thing to do is mix and match. What becomes unintuitive is jumping in and out of imperative and FP concepts and having to deal with the jarring discontinuity in paradigm.

Now if I had to pick and argument for not using JS array methods at all, it would be they're not lazy and memory abusive. But that's a completely different subject.

Collapse
 
jasterix profile image
Jasterix

That was my honestly biggest challenge with learning reduce(). My brain was too used to the syntax of for loops. But it is also difficult to understand a reduce method that's doing 3+ things in one function

Collapse
 
vonheikemen profile image
Heiker

But it is also difficult to understand a reduce method that's doing 3+ things in one function

I get it. It happens. But do you think is easier to read a for block that is doing 3+ things?

Collapse
 
mpuckett profile image
Michael Puckett • Edited

I’ve started using .map with Object.fromEntries in places I would have used reduce but it does make it look even more cryptic.

 const increment = obj => Object.fromEntries(Object.entries(obj).map(([ key, value ]) => [ key, value + 1 ]))

increment({ x: 1, y: 2 })
// { x: 2, y: 3 }
Enter fullscreen mode Exit fullscreen mode
Collapse
 
kenbellows profile image
Ken Bellows

I definitely have mixed feelings about this paradigm. I absolutely see the value in quickly mapping from one object to another, but I agree that Object.fromEntries(Object.entries(obj).map(...)) is not the most readable thing.

On the one hand, I sort of expect that it will become a paradigm that is so ubiquitous that everyone will just get used to seeing it and immediately recognize what it's doing, so readability will become less of a problem over time. But on the other hand, if it is going to become that common of an operation, I sort of wish there was a built-in method for it, something like Object.mapEntries(([key, val]) => [key, val+1]), that just did it all in one go.

All that said, it does become a little more readable when split onto a few lines:

const increment = obj => Object.fromEntries(
    Object.entries(obj)
        .map(([ key, value ]) => [ key, value + 1 ])
)

Collapse
 
mpuckett profile image
Michael Puckett • Edited

We need Object.mapEntries then I think!

Thread Thread
 
alphakevin profile image
Yi Ma

It exists in lodash _.mapValues() 😂

Collapse
 
jasterix profile image
Jasterix

Haha it does only because of the chaining to keep everything on one line. I think if you split it up as Ken does, it's a ton more effective

Collapse
 
aendra profile image
Ændra Rininsland • Edited

There's been so much .reduce() bashing on Twitter in recent days, it's really annoying given how useful and powerful a method it is.

For instance, you can rate limit a bunch of fetch() requests using async/await and .reduce() in a one-liner like so:

(async () => {
  const listOfUrls = ['https://some-api/endpoint/1', 'https://some-api/endpoint/2', 'https://some-api/endpoint/3'];

  const data = listOfUrls.reduce(
     async (acc, cur) => [...await acc, await fetch(cur).then(r => r.json())], Promise.resolve([]));
})();
Enter fullscreen mode Exit fullscreen mode

(I think; it's been awhile since I wrote one of those)

Granted, with async iterators you can do the following too now:

const listOfUrls = ['https://some-api/endpoint/1', 'https://some-api/endpoint/2', 'https://some-api/endpoint/3'];
const data = [];
(async () => {
  for (const url in listOfUrls) {
    data.push(await fetch(url).then(r => r.json()));
  }
})();
Enter fullscreen mode Exit fullscreen mode

...Or even:

const listOfUrls = ['https://some-api/endpoint/1', 'https://some-api/endpoint/2', 'https://some-api/endpoint/3'];
const data = [];
(async () => {
  const requests = listOfUrls.map(url => fetch(url).then(r => r.json());
  for await (const datum in requests) {
    data.push(datum);
  }
})();
Enter fullscreen mode Exit fullscreen mode

...But those are longer, not much more readable, invoke newer ES functionality that other people in your team might be not familiar with and use mutative array methods.

Collapse
 
kenbellows profile image
Ken Bellows

I don't know, IMO the for loop versions you wrote are far more readable than the reduce version. I typically find that to be true of for vs reduce, which is I think the point of these arguments.

(Unimportant pedantic nitpick: I think you meant to write for ... of rather than for ... in, since in loops over keys rather than values.)

Collapse
 
aendra profile image
Ændra Rininsland

That's fair! TBTH I'm sort of coming back around on using for loops over .reduce(), async iterators are really nice to use for request throttling and flow control, last project I used them in I was a bit taken aback by how intuitive they felt to use once I got into it (I've never been a huge fan of generators but I'm starting to see their value more now after that).

And yes, I definitely meant for ... of — I get that wrong so often! 😅

Thread Thread
 
kenbellows profile image
Ken Bellows

And yes, I definitely meant for ... of — I get that wrong so often! 😅

Ha, same! Especially if I've been writing Python recently, since Python's loop syntax is for x in arr: ...

Collapse
 
caelumf profile image
CaelumF • Edited

Cool article! I think Reduce is especially handy in constructing more human-friendly functions.

Kotlin:


fun main(args: Array<String>) {
    val arr1 = arrayOf(8,5, 12,90,65,1,0,768,8).asIterable()
    val arr2 = arrayOf(34,3,0,45,23,67,1,5, 15, 67,9).asIterable()
    val arr3 = arrayOf(344,23,5).asIterable()
    println(arr1.intersect(arr2).intersect(arr3))
}

Collapse
 
jasterix profile image
Jasterix

My first time reading any Kotlin code, but it's pretty clean. How would this block of code look different with reduce? Is intersect a native method?

Collapse
 
caelumf profile image
CaelumF

intersect is an extension function of Collections provided by Kotlin's stdlib, it would work on Lists, Sets, Maps (popularly implemented as ArrayList, HashSet and HashMap)

See: kotlinlang.org/api/latest/jvm/stdl...

My code could have been clearer by writing "arrayListOf" actually

Here is the code using reduce:

fun main(args: Array<String>) {
    val arr1 = arrayListOf(8,5,12,90,65,1,0,768,8)
    val arr2 = arrayListOf(34,3,0,45,23,67,1,5,15,67,9)
    val arr3 = arrayListOf(344,23,5)

    fun intersection(vararg lists: List<Int>) = lists.reduce {acc, arr -> acc.filter {arr.contains(it)} }

    println(intersection(arr1, arr2, arr3))
}

The types of acc and arr don't need to be declared, because lists is of the type List, and reduce uses type parameters in its declaration:

inline fun <S, T : S> Array<out T>.reduce(
    operation: (acc: S, T) -> S
): S

So the types passed to it must be S and T, and it must return of type S as well. Some people may choose to explicitly declare the type when type inference comes from a diffferent file.

Like you said, Kotlin is so clean! I love it. The perfect blend of concise and safe : )

If you're interested, a great way to learn is to automatically convert a Java file to Kotlin and refer to kotlinlang.org/docs/kotlin-docs.pdf whenever you're editing the file anyway, and have fun optimizing and continuing development without much downtime or needing to start again.

Collapse
 
douglasbarbosadelima profile image
Douglas Barbosa de Lima

Too many times, reduce's could be replaced by simple 'for' command. In my vision, reduce command is fantastic, but, when not used correctly, turn the code bad to read and hard to maintain by another developers.

Collapse
 
jasterix profile image
Jasterix

100% right. As much as I love writing reduce(), I hate reading it. Go figure lol

Collapse
 
miketalbot profile image
Mike Talbot ⭐ • Edited

Just to say in the example you give, this would be faster than all of those indexOf calls!

function intersection(...params) {
    return Array.from(new Set(params.flat(Infinity)))
}
Enter fullscreen mode Exit fullscreen mode
Collapse
 
sorgloomer profile image
Tamás Hegedűs

That's not an intersection, that's a union.