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 powerful method I once avoided.
But last week, I stumbled on a video by the Chrome Developers YouTube channel (link below) that challenged my stance on .reduce()
.
Is reduce() bad?
No! In my humble opinion, it was clearly the greatest thing since sliced bread. Not only can you use .reduce()
to well...reduce, but you can use it to map and filter. You can use .reduce()
to keep tall, to cross-reference multiple arrays, to flatten arrays and more!
How could reduce()
possibly be bad?
Well, as the old adage reminds us– just because you can, it doesn't mean you should. So I watched the video. And 2 minutes in, I understood the point of the video's initial questions.
While .reduce()
is a very powerful method, it's not always the easiest to read or grasp at a glance. In fact, reviewing some of my most efficient .reduce()
functions, it took me a bit to unravel some of the logic.
As Jake explained in the video, 'the simplest code is written sequentially'.But the reduce method is ordered in a funky, nonchronological way. The syntax for .reduce()
is:
array.reduce((accumulator, currentValue, currentIndex, array), initialValue)
To understand how the method below is executing, you would need to start at the end for the initial value, then take one step back into the function for the operators and then look at the accumulator to anticipate the result.
const reduce = (arr) => {
return arr.reduce((accumulator, currentValue, currentIndex, array) => {
return accumulator + currentValue
}, 10)
}
reduce([10,23,5]) // result = 48
When should you .reduce()
?
You should be using reduce for actual reductions, meaning you have an array that you want to reduce to one value. This should be the value held in your accumulator.
When should you not use .reduce()
?
If you're not sure how to differentiate between when .reduce()
is or is not appropriate, ask yourself-- can the behavior you're trying to execute be achieved by instead using:
- map
- filter
- loop
It could be that .reduce()
is the more elegant solution for a particular task. In those instances, also ask yourself-- does reduce make my code more or less readable?
Discuss:
You're given three arrays and asked to return the numbers found in all three arrays. Looking at the code snippet below, would you use .reduce()
or is there a better solution?
let arr1 = [8,5, 12,90,65,1,0,768,8,4]
let arr2 = [34,3,0,45,23,67,1,5, 15, 67,09]
let arr3 = [10,23,5]
const intersection = (input) => {
return input.reduce((acc, arr) => {
return acc.filter(num => arr.indexOf(num)!==-1)
})
}
console.log(intersection([arr1, arr2, arr3])) // result = [5]
Check out the Chrome developers video I mentioned earlier.
Also, take a look at this blog post by the Khan Academy engineering team. Josh Comeau offers some great examples of how to use .reduce()
.
Top comments (31)
reduce
is not bad, it's just unfamiliar. In the javascript communityfor
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.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()
... Simplefor
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 areduce
function is doing, where a simplefor
loop would have been trivial to follow.Sure the amount of content about
map
,filter
andreduce
has increased over the years but that is something you learn later down the road. I would imagine thatfor
andwhile
loops are still the prefered way to teach beginners about iteration.To be fair
map
is actually quite useful.I like that they show cases where
reduce
is completely unnecesary, but that doesn't makereduce
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.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.
That was my honestly biggest challenge with learning
reduce()
. My brain was too used to the syntax offor
loops. But it is also difficult to understand areduce
method that's doing 3+ things in one functionI get it. It happens. But do you think is easier to read a
for
block that is doing 3+ things?For me, the simple fact that I learned
for
loops first has meant that my brain defaults to it. I'm more fluent withfor
loops. So any other syntax means having to "translate".If it offers little to no value, as .reduce does, then the easier/simpler way has to be considered the best.
Why do you think
.reduce
has little to no value?.reduce
follows a pattern and has a use case, just like.map
and.filter
.Almost any use case reduce has is best served by a .forEach or through .map or .filter. Most of the time it's useless complexity
It really isn’t a good comparison though, as reduce has a different use case. For instance, reducing an array into a single value would not be possible with map nor filter (hence the name reduce).
It is possible to do what alll three of them do with forEach, if you create a variable holding the result outside of the callback function, but personally I think that looks worse and disconnects the result from the operation.
Since "reducing to a value" is rather ample, i.e. the value could be an array, an object, or whatever some use cases can be supplemented by
.map()
and.filter()
.Regarding the forEach, yep that's true, all of them could be replaced, but reduce is the only one that actually adds complexity and I'd argue it's the one that adds the least value.
.map()
and.filter()
reduce complexity from reading the code,.reduce()
adds to it and that's why I don't favor it much. At least, besides simply summations, multiplications, etc I haven't really seen a case where I'd feel it was a good use yet.In what way do
.reduce()
add complexity while.map()
and.filter()
reduce it? It would be awesome if you'd care to expand on this. 😅Ofc,
.map()
and.filter()
are very clear on intention and have a simpler API to grasp. e.g.:*Taken from *
While .reduce() is less readable (unless when used for things that are simple like simple arithmetical operations). e.g.
Taken from: developers.google
Of course, those are examples, and we can find ones in which
.reduce()
wouldn't harm readability so much as the one above. But for most use cases I've seen in the wild, it does. It simply is harder to read and reason about to a lot of people. That's why even though I can understand it, I steer clear from using it. In general, Code will be read a lot more times than it will be written so even when we have to add one or two extra lines, they are usually easily paid for ten-fold by gains in readability :)Funny that you say that because
.reduce
is a really good name. It can be used for many things but the ideal use case it's in the name.Sure, both
.map
and.filter
are easier to explain because they focus on individual items and nothing more. Now.reduce
is another story, it can transform the entire array into something else so you can say that it has a bigger scope.About those snippets.
Showing complex (sometimes bad) examples and putting the blame on
.reduce
doesn't do anyone a favor. It makes harder for people to see what is good for.That's what I'm talking about, it doesn't look like you know when it's safe to use. Arithmetical operations are just an example, the pattern that you should be looking for is a function that combines two values of the same type. In the second snippet on
logInOrder
, that is exactly what happens. We can improve the readability on that snippet if we recognize that pattern.The first thing we do is stop trying to do everything in one expression. The other thing you might notice is that the callback
chainPromises
doesn't treat the data like a special snowflake inside a.reduce
, it's a generic function that takes two promises and returns a new one. When you add.reduce
into the mix it should become clear we are reducing an array of promises into a single promise. That said, I still think this is a bad use case for.reduce
because the goal of the function is to produce a side effect, that alone should exclude it as the right tool in this case.I want to say more but I think this is enough for now. If anyone wants to know more, I wrote about it here: Reduce: how and when.
I’ve started using .map with Object.fromEntries in places I would have used reduce but it does make it look even more cryptic.
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:
We need Object.mapEntries then I think!
It exists in lodash
_.mapValues()
😂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
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:
(I think; it's been awhile since I wrote one of those)
Granted, with async iterators you can do the following too now:
...Or even:
...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.
I don't know, IMO the
for
loop versions you wrote are far more readable than thereduce
version. I typically find that to be true offor
vsreduce
, which is I think the point of these arguments.(Unimportant pedantic nitpick: I think you meant to write
for ... of
rather thanfor ... in
, sincein
loops over keys rather than values.)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! 😅Ha, same! Especially if I've been writing Python recently, since Python's loop syntax is
for x in arr: ...
Cool article! I think Reduce is especially handy in constructing more human-friendly functions.
Kotlin:
My first time reading any Kotlin code, but it's pretty clean. How would this block of code look different with
reduce
? Isintersect
a native method?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:
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:
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.
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.
100% right. As much as I love writing
reduce()
, I hate reading it. Go figure lolJust to say in the example you give, this would be faster than all of those indexOf calls!
That's not an intersection, that's a union.