Something had to be said. From time-to-time I see a post appear that goes along the lines of, "Loops are bad, you should be using filter, map and r...
For further actions, you may consider blocking this person and/or reporting abuse
Thanks for sharing your view.
I have to say I disagree on your basic premise, though.
This is a misconception, in my opinion. Although immutability does indeed contribute highly to readability and reduces bugs, the main benefit of this kind of list processing is in how it communicates intent.
With for loops, every example is different. Each loop must be read, parsed, and mentally executed in order for the reviewer or maintainer to understand what it's for (pun!) The only exceptions to this rule are for loops that closely mimic the behavior of map, by deferring their bodies to some function, but even in those cases your stuck with the semantically useless
i
variable in the body.Consider
The only part of line 2 with any semantic meaning is
things
By contrast; map, filter, and reduce always mean the same things. They are a shared interface representing classes of procedures. forEach communicates the intent to do side effects, map to mutate, reduce to produce a single value from multiple values.
To my eyes, this is vastly superior at communicating intent. The amount of verbiage in the first example... Who has time for that?
And remember that the first snippet is the best case of for loop. In most cases, you'll have to read several lines of imperative statements in the body in order to make heads or tails of what's going on.
That being said, just typing
.map(
won't magically solve all of your readability problems. Naming your functions semantically is key, avoiding lambdas in almost all cases helps.Ultimately, one needs to adopt the mindset and goal of communicating their intent from the most to least abstract steps, starting at their program's entry point (usually at the bottom of the file) and working their way down.
Now that doesn't mean we should dogmatically forbid all loops. You're right when you say that loops can improve performance in cases. But let's not sacrifice semantics, maintenance, and readability for performance gains which may not be necessary or helpful.
Write semantic, well-abstracted code first. Later, identify and ameliorate performance bottlenecks on a priority basis.
Thanks again for kicking off the discussion!
This is a great response. You make valid points about these functions communicating intent. I don't think there is anything wrong with these functions because I use them quite a lot myself. It's only when I am dealing with large arrays of data (which is not all too often) that I will choose a for loop.
I agree a for loop where the index is defined in the loop and incremented looks horrendous. I use
for..of
in combination withArray.entries()
these days if I need a loop and the index value.Your example could be cleaned up considerably using a
for..of
loop and doing the following:You could even go one step further and not bother with the
newThing
constant and just do this:I am not using
Array.entries()
here because we don't need the index, we just want the value inside of the array. You can't argue thatmap
does not look cleaner and in this instance, I would also usemap
as well. Ifthings
was comprised of 50,000 things, I might think twice, but a few hundred or even thousand, I would stick withmap
.I definitely agree though, as I said at the bottom of the article. Write your code now and optimise it later on if it becomes a problem. Chances are you're only going to incur 100 or so milliseconds using
map
or any other method, to the point where you or anyone else wouldn't even be able to tell.So you're saying you have a nuanced and reasonable approach to a twitter-hot-take-bait topic? That's crazy-talk! ๐
Thanks for the post, I have had similar thoughts about this topic but it seemed like I was in the minority in feeling that way about these functions. I think it is seen and taught as the "modern ES6 way", but as you mentioned these are functions added for convenience/functional programming and not the new default for looping. I think the appeal is that it looks cleaner and the code can be shorter, but I'm a fan of easy to understand code as opposed to one-liners.
In addition to your examples, I have also seen some roundabout ways of using these functions when there is a simpler and more performant way.
map
,filter
, andreduce
) on the same array. I cannot remember the exact use case, so it may have been valid, but I'm confident there was a simpler and faster way to achieve the same result.It has definitely been at the forefront of my mind for a while. I think whent he hivemind collectively says something is good or bad in the front-end community, it tends to spiral out of control and the true meaning of things is lost.
The ability to write shorthand (and harder to understand) code that looks like it is being submitted as a competition entry into a code golfing contest definitely seems to be associated with these methods. Admittedly, you can't deny it's not shorter, but I prefer verbosity whenever I can because I cannot stand it when developers try and be clever to save a few extra characters or lines.
100%. I have seen this a lot as well. With the advent of GraphQL, filtering data like this is even easier now as well. Whenever I can use GraphQL queries, I always make sure my resolvers support additional parameters for filtering the data server-side instead of on the client.
Oh, man, this makes me sweat profusely. I've seen this before.
This chaining is known as the holy trinity in the functional programming community, chaining for some is like a religion they go to sleep at night and they have dreams about chaining their inefficient non-side effect producing functions while the performance drains from their apps.
In lazily evaluated languages,
[1,2,3,4].map(add2).filter(isDivBy(3)).reduce(+)
doesnโt do anything until you ask for it.Another example:
x := [a,b,c].map(g).map(f)
does nothing, but callingx[2]
will returnf(g(2))
(doing the calculation and caching it when called)This allows you to work with things like infinite (or practically infinite) without the cost of running your calculations on infinite things.
It also allows the compiler/interpreter to optimize the
f(g(x))
into a new functionfโขg(x)
. Example:f(x)=x+2
;g(x)=x / 4
;h(x) = (x/4)+2
Thenf(g(x))==h(x)
(coincidentally, this is why encrypting something multiple times is technically equal to doing it once with a different function)Now thatโs not to say if you know h ahead of time that you shouldnโt just write h. BUT, if you have a bunch of functions that do f, itโs often easier to reason about if you extract the logic thatโs different and define g, gโ, g'', g''', etc and just pass them in.
for
only makes sense in languages where you have to manage memory yourself and/or you don't have easy access to true higher order functions.Once you start passing functions as arguments,
fold
and its children make your life way more easy to deal with.Take this contrived Haskell example:
Ok, that's unreadable... let's convert it into its rough Javascript equivalent:
I know it looks like a lot, but it's a REALLY powerful concept that lets you make even MORE reusable things while not having to increase the complexity of the underlying building blocks. For small examples like this, it seems like a lot of overhead, but combined with a strong type system (and tail call optimization) you can more easily make trivially correct programs.
I wish I had the words to convey this better, but it's part of what makes functional and declarative styles so powerful and so difficult to get into after really learning the imperative style. Also, the fact that languages like c, Javascript and Python have dealt with recursion and corecursion so badly in the past hasn't helped us either.
fold
really relies on recursion as an underlying concept.Interesting as I responded to a similar inquiry.
forum.dlang.org/post/suinxiufqltip...
But it was with a language where libraries are written with generic iteration. Yes arrays are used heavily but only stack allocation for this example, with early exits and multiple filtering.
But your context is Javascript, and context is important because as you point out it costs where functional languages can play tricks.
I just want to note that JS could be a better language if they'd just learn from others, and the community could get behind doing things better.
For breaking a loop in javascript you can use .some() functional method, which return false as default, but you can return true (with a break functionality), when your condition passed. For example:
const arrayList = [false, true, false, false];
arrayList.some(arrayItem => {
if (arrayItem === true ) {
// your business logic...
return true;
}
});