We use XO for our code linting. Recently I upgraded to its latest version and suddenly I had lots of errors as soon as I tried to commit (**).
What was wrong?
Well. It seems that there is a new trend out there.
Array.reduce is the new most hated guy.
It is disliked so much that a new ESLint rule was added to prevent - or reduce its usage.
What the heck!
I remember that when I started using it 3 years ago, it took me some time to understand the use case and find it cool and useful. And now, even though I don´t use it so often, it generally makes the code look quite nice and smart. Until now, I guess.
When I found all these eslint errors I was quite pissed, first because they were unexpected and I did not want to spend time fixing my code, nor cluttering it with eslint-disable
comments to ignore it. But I was also quite intrigued by the reasons behind this opinionated choice from AVA contributors.
I read some of the comments in the thread and started reconsidering the snippets in our repository that contain Array.reduce.
Let´s consider this simplified example, where we have a list of records and we want to validate them and aggregate all the valid and invalid ones.
const isValid = (record) => // run some validation logic over the record props and return true or false
module.exports.analyzeResults = (records = []) => {
return records.reduce(
(acc, current) => {
if (isValid(current)) {
acc.valid.push(current)
} else {
acc.invalid.push(current)
}
return acc
},
{valid: [], invalid: []}
)
}
With Array.reduce we can achieve it quite nicely, with one iteration only over the list and returning 2 new arrays.
What would be the alternative without Array.reduce and using Array.filter and Array.map instead, to still be as functional as possible?
module.exports.analyzeResults = (records = []) => {
const valid = records.filter(r => isValid(r))
const invalid = records.filter(r => !isValid(r))
return {valid, invalid}
}
```
I know already what you are going to say:
> Ehi, but you are iterating over the list twice!!
True.
But the code is undoubtedly simpler and nicer to read.
So to some extent is the same objection many devs still say when it comes to use {% raw %}`
array.map(simplifyDataStructure).filter(bySomeProp).map(extractOnlySomething).filter(whatIwant)`
against doing everything in one single For Loop.
> Readability and Testability of the single operations
So unless you have a very very big dataset, it is really better to favour readability or simplicity rather than stuffing everything in a _complex_ reduced method.
I am not entirely sold on the new trend. And I am not going to rewrite all my methods using Array.reduce, but this discussion really tickled my interest and helped me question my stance and coding.
What do you think?
---
(**)
> ProTip: Use [Husky](https://www.npmjs.com/package/husky) to create a git hook so that whenever you try to commit XO is run and your code is linted. that really helps to enforce coding standards in your team, and prevent unnecessary pickiness during code reviews.
Top comments (81)
Nothing. Most people are more familiar with imperativ or object orientated programming, so so functional programming practices are unfamiliar. Unfortunately for some developers unfamiliar automatically means it's wrong.
This about sums it up.
Once people are familiar with reduce, it looks just like any other boiler-plate. The callback function used is the "reducer". I would find it weird if I saw React people preferring the old for-loop.
I think the test-ability argument is a moot point in the thread, because if there are many conditions and a really fat reducer function, that needs testing, not the reducer in the context of a reduce invocation.
An argument can be made on both grounds. Functional AND declarative code is typically preferred. A lot of people have already used this example but in a less readable fashion, but let's really illustrate declarative code:
Calculating
lcm
of more than two numbers also becomes impressively simple and beautiful. For example my AoC day 12 solution.☝️
I like this approach (plus I finally understood where reducer comes from :D - in .NET it's called Aggregate). However, I also think that if one is not going to (re)use multiple types of reducers, there is no need to use reduce to emulate a forEach or even a normal loop.
But congratulations on the conciseness of your code!
Yeap! I've never done C#, but the analogs are listed in this SO thread: stackoverflow.com/questions/428798...
Yeah, it was a contrived example from many people in this thread. I also don't use JS regularly at all anymore which is why I often default to ES5 syntax.
I typically end up using reduce for "partitioning" or performing only a single iteration through the collection to include/exclude/filter into a different data type, like a set, map, etc to keep things atomic... we've all seen people declare combination of multiple arrays, array and object, array and map, etc at the TOP of the file and the actual for-loop is modifying it 100 lines down, and some other code that modifies it in between... that's really really really bad and shitty for anyone needing to debug it. These functional facilities over collections make them atomic and you know where to look if there's a bug -- the reducer (hopefully unit-tested)...
I've used reduce for a lot of this (changing data-types, and accumulation to a different collection type)
Some people call this "partitioning". The above would be difficult to express with just a filter call or for-loop without scattering variables. The thing that gets most people is where the "initial value"
{evens: [], odds: []}
is. People are just very used to seeing it right above an iterationI see what you are saying, but unless you are going to reduce that function, this can just as well be expressed by a forEach or a loop. One declaration of a variable above it doesn't bother me. Cheers!
forEach
just abstracts away the iterator variable boilerplate, but could still leave the collection variables scattered about, like at the top of the file and make it possible for someone to mutate them in between... but yah!Pardon for jumping in but fold are very well studied functions, as you know :) Sorry for dropping a link, but it speaks much better than I could: Fold Higher Order Function
@wulymammoth your code is slightly wrong. You have to return the accumulator (the object of arrays, this case). You're instead returning whatever push returns (which is the length of the array after pushing). (Also, you're re-declaring the parameter
num
as a const, which isn't allowed, but that's a simpler fix.)All good catches
Yeap! Just re-using the maximum example that others have used for illustration purposes only. JS already has this available in the standard library :)
Exactly! I reckon anything overused is bad. However to straight up deface the enormous power a fold function gives is just wrong itself. Structural transformations are a thing, and there's a tool for them, period.
Reduce is fine in my view. A little bit tricky, but you can solve many problems. Main meanings: const result = something.reduce(reduceProcess, startingValue).
for example pipe function:
There is nothing wrong with
reduce
, it's only sin is to be slightly more difficult to understand than afor
loop. The arguments againstreduce
can be reduced to this phrase.That's it. Anything that you can do with
reduce
you can do with afor
loop, but since thefor
loop is the "simpler more readable" choice then it must be the right one.In this case simpler and more readable is very subjective.
Is it not safe for us to say that once you understand how the
reduce
function works, then reading/understanding it becomes quite easy.I agree that the loops are more readable though but I don't think it's enough to tell folks 'never' to use
reduce
That's why it's wrapped in quotes.
Agree. Readability is anyway mostly a matter of being used to read such code. :-)
mmm. but this is true also for array.foreach and array.map etc. and i find those much nicer and simpler. I love reduce. but i kind of agree with the rant in the tweet to some extent
I'm just repeating the only argument against
reduce
I think it make sense. By some misfortunereduce
doesn't seem to "click" with people in the same waymap
,filter
orforEach
do.which is a shame, because
map
,filter
, andforEach
are justreduce
in disguise.Though, for completeness, all of those are just loops in disguise (which isn't an argument for or against it).
Only if you consider loops and tail recursion to be the same thing, which is debatable.
Also, it's only the same when applied to lists.
Other monadic structures may not act as loops at all, when called with
reduce
akafold
This isn't a very good use of reduce, in my opinion, in any case.
In almost every case where you have an inlined reduction function, you're better off using a for loop, because it is less complicated.
Simple, stupid, and gets the job done.
Your use of reduce just adds extra rubbish in the way, and is really a kind of abuse of a reducer in any case -- you're destructively updating it in-place -- so why not just do that in a more obvious fashion?
However, had you defined a reusable reducer, would have been a bit different.
Now it's starting to look much more reasonable.
And let's get over this "in a functional way" nonsense.
array.reduce isn't functional -- it's just an application of procedural abstraction, there's nothing functional about it.
At best you can say that it's procedural code being used in a 'functional style', except that, really you can't say that, because you're not using it in a functional style.
So it's a bit like building a run-way in the jungle in hope that presents will magically drop out of the sky. :)
Correct, no function on its own is "functional" programming.
"Functional" is only when a function is useable in a composable manner, meaning it can be composed with other functions to make larger functions.
For example, (and somewhat ironically), the "reduce" in python can be used functionally (it may have to be curried first), but not the "reduce" in JavaScript, because in JavaScript it's not a "free-function", it is bound as a member method of the Object blueprint, Array (essentially a Class). Thus in JavaScript's case, it's reduce "method" is Object Oriented. Also, a function that takes a function as a param doesn't simply make it formally "Functional Programming"
Of course you could make it a functional-programming-usable function by wrapping it in a free-function.
For example...
Can you give an example of a function that cannot be composed with other functions to make a larger function?
Putting aside the issue of javascript functions being procedures.
reduce isn't bound as a member -- you can use it independent of Array, and you can supply any 'this' you like when you invoke it.
Effectively there is an additional argument, which provides the value of this.
I think perhaps you are trying to understand javascript in C++ terms?
Javascript doesn't have a distinction between member-functions and free-functions.
And even if it did, being associated with a class is not in-and-of-itself relevant to being a function or functional.
All you're doing here is some syntactic hopscotch -- it's calling array.reduce normally, which should tell you that you haven't changed anything fundamentally.
But it's getting clearer that what you consider to be functional is just one particular kind of procedural composition.
What's significant about functions is that they're time invariant.
Oh but it is. Have you ever heard about Foldable?
If it were, you wouldn't be able to go around pushing things in reducers.
As it is, it's just an example of higher order procedure application.
You did read the thing, right? If you didn't, please don't let the name stop you.
I read it.
Do you understand the difference between functions and procedures? :)
Function like in the mathematical sense? Sure, I like to think I do.
I haven't used array.reduce enough to really have an informed opinion on that specifically. In my job I don't write core code, I merely maintain and review it after the engineers move on. 90% of the code I read isn't my own and in order to be cost effective I have to be able to read and understand what was written.
When I worked in video games, I can also remember a number of times where an engineer would leave and we'd have meetings pulling various engineers in to decipher some random piece of code. The succession plan of your code is greatly improved if it's simple and readable. I imagine Open Source products benefit in the same fashion.
From that perspective, it might not be just preference that influences the decision but a business decision to favor code that is simple and readable over complex and/or elegant.
I don’t mean this as an insult, but I don’t believe any developer goes in saying, “I’m gonna write cryptic, concise, and unreadable code”. As you can see reduce is not unfamiliar to a lot of people. It is unfamiliar mostly to those that haven’t explored functional languages. In fact, underscore.js and lodash.js provided these utilities before they were a part of the standard library in JS.
I would argue that it is never a business decision to favor code that is simple and readable. The business/org doesn’t care about the implementation details — it is the dev team that will constantly be reading it. So if the team is unfamiliar with a particular style, keep it consistent. It’s fair to argue that everyone knows the standard boilerplate that’s imperative with a for-loop. But for growth and maintainability, which can be orthogonal to “presently readable”, I’d challenge the constant use of for-loops.
Some people explore different languages so they can express things better, but do realize that — familiar != better. It’s subjective. Someone that doesn’t have access to a large vocabulary may need a whole sentence to describe something that could perhaps be described in a single word. This is what this essentially comes down to. I mention this because this has a real impact on source lines of code and for most people, less code == less maintenance, and less surface area to search across when a bug arises. So there is a preference from my obviously biased perspective for people to expand their “vocabulary” and then decide when the right time or wrong times to use something are.
Furthermore, I much prefer people to use the filter method on arrays rather than seeing a General for-loop that requires me to read the body of that loop to decipher that what it’s actually doing is filtering. Declarative > imperative
I didn't mean to imply that developers intentionally make there code complex, but the OP did mention that he initially favored array.reduce because it "looked smart".
The barrier to understand code isn't necessarily subjective. A for loop in most cases is objectively more widely understood than array.reduce.
If you work on a team where you aren't going to be responsible for maintaining your own code and you aren't in control of who will, then choosing a simpler more readable approach might be something you'd consider. From a business perspective the more time someone spends trying to understand what you wrote the more expensive what you wrote becomes which depending on the team you may or may not care about.
Okay, you’re making the same argument Objectively easier? No. I disagree. Familiarity, yes. Give the example of filter or reduce to a non-programmer and ask them what they think is happening. The for-loop is a familiar construct for those coming from C-style languages.
So you’re still arguing on the basis of “simpler and more readable”. It is subjective, because if I’m the audience, it is far simpler and more readable to me to see a reduction than have to read through the body of a for-loop to understand what’s happening. What you’re really stating is to consider writing in a way that covers the broadest audience.
Let’s take another concept that’s often hotly debated — recursion. Should we avoid recursion because we want to provide the most accessibility? Honestly we can just use for-loops and nested for-loops to express a bunch of ideas that are recursive or repetitive in nature. The fallacy in your stance is using “simpler and readable” because what is simple and readable is different to different audiences of different levels of programming ability. Therein lies why it is subjective. By golly if someone wrote a bunch of for-loops to express a recursive operation that is elegantly expressed in a simple recursive function with a base case would drive me nuts. The only consideration would be performance here — memo-ization and dynamic programming is far more performant and changes recursive functions to iterative and performant ones but is far more difficult to understand
Just to be clear, my original point was: If you're given a choice, simple and readable might be something you'd consider because there might be a business advantage.
//edit I also unfairly edited my response to you prior to you responding. Apologies for that.
"Consider your audience" is probably the point I was trying to make. Thank you for talking me through that.
Fair -- whatever "simple and readable" means to the developers...
But again, I, too, responded to that bit -- "business decision" or "business advantage". Unless you're in the business of writing code for developers to read, this is a bit of a moot point. Right? The business does not care about the code -- only the result of that code. The consumer of an iOS or Android app does not need to know whether the developer has used for-loops or reduce in the code...
Open Source might be an example of a situation where a single dev might consider there audience being other devs.
I also don't think it's fair to say businesses don't care about the code. I just don't think it's practical for most businesses to care. A business might empower someone to make decisions on their behalf; "Lead Developer" comes to mind. A business might document how it expects it's developers to code, Google's Style Guides for example. Either of those might care whether for-loops are favored over reduce.
I don't think many developers appreciate the succession of their code. I also think, many developers don't have to. If you're a single dev, writing a single app, for a small business who doesn't care about anything except the app being delivered on time. Your audience and priority is obviously not other devs.
Yes. Familiarity and audience are the key here. In our team everyone is able to understand a reduce. And everyone likes it over a forloop. But now that I rewrote it with filter and map it looks more readable than reduce. So it depends. I really like the conversations here. Thank you all
Communication in a team and outside both matter. It's always about the audience and sometimes that communication means getting each other on the same page. Filter and map are less generic than reduce when maintaining the same collection data type. Reduce is typically used to "distill" or bring it down or fold the collection (array) into something else. It's much more clear to map and filter in many instances because that's exactly what we're doing -- we still want to retain the collection (array). Whereas some other operations are: "reduce to an integer/sum/max", "reduce to a different data type/object", "partition" that are better expressed as reductions that map and filter can't do.
If it's a mapping operation or filtering operation, I'd much prefer to see map or filter. If I were reviewing that code and somebody threw a reduce to do what a map or filter call does, I'd ask them why...
But generally, I see folks throwing the for-loop at map and filter operations as well -- procedural and imperative. I'd much rather the code state what it's doing (by declarative means) than have me read through the body of the for-loop to determine that what is being done is a reduction, a mapping, or a filtering operation. The for-loop is much too generic outside of small use cases.
For one place that I've worked, we always asked people to use reduce, map, and filter. If a for-loop is used, that means it's a signal to other developers that there is the possibility of early termination (not iterating across the entire collection). So we have all these use-cases broken down and documented, otherwise the for-loop can be ANY of these four operations and requires the developer to read the body of the for-loop. Accessibility is high if the audience is broad, but in a team-setting with established patterns, this is just overhead...
Indeed. Early termination (or very huge datasets, where I really need/want to iterate only once) is the only case where I use for loops. Otherwise it's always map and filter + reduce sometimes.
I didn't mean "businesses don't care" to be a sweeping statement, but again, it comes down to the audience.
When you're bringing it into the arena of empowerment, it's almost an entirely different discussion. A lead developer or senior dev or engineering manager typically help bridge the gap between product managers/stake-holders which I perceive to be "the business". To me, that's divorced from the implementation as stakeholders at companies that I've been at have no say and won't even be able to read code. Even if you take Sundar Pichai and ask him, unless he's worked in a language, he's probably not going to have a valuable opinion on how some deliverable should be implemented.
When we're talking about style guides, these are created and consumed by developers and if you're in the business of building for developers (I currently work on APIs for devs), it is important that everyone is on the same page about these sorts of things. As someone that also contributes to documentation, I'd probably share both examples and let the end-consumer decide which is best in their case, but in the source code of the library, follow the established or agreed-upon style guide (if any) internally.
And you're totally right in your last paragraph -- I think this entire discussion started on the premise that our code is intended to be read by others and why reduce may be bad. But like any tool, it's just a tool and they can be used well or poorly. Someone stuffing a massive reducer as the callback to
Array.prototype.reduce
is a code smell, but so is stuffing that same conditional logic within the body of a for-loop. And if there are multiple declarations an initialization of collection objects (arrays, objects, sets, map) that the for-loop will be mutating scattered about on different lines, it's going to be very difficult to keep track and who knows if between those lines some other code is mutating it before it reaches the for-loop. Reduce keeps things atomic. There are a segment of folks that will tell you that the readability trade-off for atomicity is HUGE, because debugging won't be an issue with reduce as you're sure that mutations happen in ONE place. The easy thing to do doesn't always yield the desired result. The one thing that is constant in software is change -- the more churn and change that happens to such code will inevitably result in a regression that becomes hard to track down, fix, and/or refactor. Look no further than scripts. Scripts are ad-hoc and implicitly coupled to the very specific use case the author had in mind, but are not maintainable by a team of people. This is why patterns exist and abstractions and sometimes it is good to force the wider audience to learn and adapt to them if there is merit and that merit being less pain later on. It's not to "be smart". Those that are deeply down the functional paradigm can be assholes but unfortunately what they spout is too far to bridge the gap between what most people are used to and what they've come to realize (useful and arguably better). But I think core functional facilities like reduce strike a good balance and it's best that people are familiar with when and why to reach for it and also not be taken aback when it is used. Lastly, they should also call out when it is abused -- stuffing a massive callback/reducer. Just rip it out and give that a name and unit-test the reducer in isolation. It's notreduce
's fault that someone decided to write an anonymous/lambda/callback that is untenable and unreadable. People are arguing about the wrong things here...Awesome comment. Thanx
The way I see it, it's mostly familiarity bias.
reduce
will be harder to understand to someone who is not used to it and, well, since anything that can be done withreduce
can be done with a for loop, why learn a new construct?Also, regarding your example, I think a better functional primitive to use here would be
partition
. Unfortunately, Javascript doesn't havepartition
out of the box, but it's a function that takes a predicate and a list and returns a list with all the elements for which the predicate evaluates to true and another with all the elements for which the predicate evaluates to false.The implementation is straightforward with
reduce
:Or you can take it for instance from ramda.
And then your
analyzeResults
is just a one liner:This is to show that, while
reduce
is super useful, there are still a lot of functional primitives missing in Javascript, which forces people to use less elegant solutions, and might contribute to the perception ofreduce
being hard to read.I created a dev.to account because examples like these are mind boggling. It seems to me that reduce works great for the mathematically-minded and can parse that function very easily, but have to ever stopped and thought about the alternative for-loop? Because to me this looks wildly more readable and it's guaranteed to be more efficient too (no array creation at every loop)
very good point. thanx for commenting
This!! Yes! I actually used reduce in JS and Ruby to partition in the past
I agree your example is one where
reduce
is better expressed in other ways. But I think there's cases wherereduce
is the correct metaphor and the clearest construction:I will never be convinced that a for loop is more readable than that (and
map
andfilter
aren't options for this type of computation)I almost always to write first argument as
prev
oracc
; therefore,About readability, I guess it depends on how you visualize.
This seems like a very personal choice. I personally am a fan of reduce as it forces me to identity what I'll be "managing" over the loop. I also don't usually use many of the
for
loop alternatives, even though they are essentially just as capable.A perk of JS is its flexibility, so there are multiple ways to do the same thing. As a side-effect, people will always have opinions on which is the right way.
If you like reduce, keep using reduce. If someone else hates reduce that isn't on your project, don't worry about it. If they are on your project, then decide the best way forward together :)
I don't think reduce is bad, it's just the way some use it to make it more complicated than it should be. Those same things can be done easily with a for loop and with the inclusion of the
for ... of
loop, it becomes quite easy to perform those operations.Jake actually has a video for the same and you can see that how simple use cases are over engineered with reduce.
I also agree with the point that reduce has its parameters reversed, the callback should always be last, but that's a personal opinion.
The JavaScript community continues to follow the trend of syntactical additions being requested, added, praised, and then criticized because of unreadability and mental overhead. If you want to use
reduce
because it fits your context and provides clarity, then use it. The problem is that too many people misunderstand why the convention was initially introduced — not to provide a "swiss-army knife" replacement forfor
loops, but to alleviate complexity and provide more purity when writing functional code.