DEV Community

Dwayne Charrington
Dwayne Charrington

Posted on

Stop Telling People For Loops Are Bad

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 reduce instead" - it makes me grind my teeth every time I see a post try and argue that you should be using these functional methods for looping everything.

Yes, these functional methods have a purpose and valid use. I am not arguing that you should not use them, but I am arguing against using them for everything. In many instances, if you want to write performant code, a for loop is always going to be the faster option.

The whole purpose of methods such as map, filter and reduce is preventing the mutation of what gets passed into them. In FP (functional programming) style, passing in an array into these methods will return a new collection and leave the original untouched.

In the case of map (because it seems to be the most abused and least performant) the difference can be significant in comparison to a for loop. The reason is map will create a new copy, fire off a callback on every iteration and consumes more memory. A for loop is working off of whatever you're iterating, so it has no overhead almost.

Someone has created a repository where they have done the comparison work, the results for large results of data are astounding (and not at all surprising if you know how these methods work).

In my web applications, I tend to use map, filter and reduce a lot more these days than I used to. I am mindful however that a for loop is a better option in some instances because a for loop can do things these methods cannot.

Breaking the loop

You might already know this, but a for loop can be stopped using the break keyword. If you're using a for loop to iterate over an array until you find a specific item or a certain number of iterations have run, you can stop the loop by executing break in your loop.

On the contrary, map, filter and reduce by their very nature cannot be stopped; they will iterate until they have gone over every item in your array.

Asynchronous loops

A for loop can easily be used with async/await, meaning you can pause the loop and make it wait for a promise to resolve before proceeding to the next value being iterated over.

Functional methods map, filter and reduce are turbulent when you try throwing async/await into the mix. I think you can get it to somewhat work in a reduce if you await the accumulator, but it doesn't feel right.

A for loop makes it easy, so why make things harder on yourself? And yes, async/await works with all kinds of for loop.

Are you doing this? Stop it.

async function loadValues() {
    let myValues = await getValuesFromApi();

    myValues = myValues.map(value => {
        value.total = value.price * value.quantity;
        return value;
    });
}

The number of times I have seen a map used to iterate over an array of objects, only for the result to be reassigned to the very array being mapped is too many to count. The whole point of map is to create a new copy of the array, except many people use map as a loop and reassign the result back to the original variable instead of a new one.

If you are doing this, stop it already. Just use a for loop, because that is exactly what you are doing and you are gaining no benefit from using map in this instance.

Conclusion

In many instances, you won't notice a performance hit if you use any of the aforementioned functional methods over a traditional for loop. If you're not dealing with tens or hundreds of thousands of pieces of data, you won't be able to see a 100ms difference.

Write whatever you are comfortable writing and then if you notice performance is less than ideal, you can use your browser developer tools and common sense to pinpoint which areas of your application could be optimised.

Remember: premature optimisation is the root of all evil

Latest comments (9)

Collapse
 
krivkoo profile image
Matej Krivonak • Edited

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;
}
});

Collapse
 
jbristow profile image
Jon Bristow • Edited

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:


isLetterA :: Char  Bool
isLetterA c 
  | c == 'A' = True
  | c == 'a' = True
  | otherwise = False

stripChars  (Char  Bool)  String  String
stripChars cfn = filter (not . cFn)

stripChars C.isSpace "I made a string!" -- Should return "Imadeastring!"
stripChars isLetterA "I made a string!" -- Should return "I mde  string!"

Ok, that's unreadable... let's convert it into its rough Javascript equivalent:


function isSpace(c) {
  return c == ' '; 
}

function isLetterA(c) {
  return c == 'a' || c == 'A'
}

function negate(boolFn) {
  return (v) => !(boolFn(v));
}

function stripChars(charFilterFn) {
  return (s) => s.filter(negate(charFilterFn));
}

stripChars(isLetterA)("I made a string!"); // should return "I mde  string!"
stripChars(isSpace)("I made a string!"); // should return "Imadestring!"

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.

Collapse
 
jessekphillips profile image
Jesse Phillips

Interesting as I responded to a similar inquiry.

forum.dlang.org/post/suinxiufqltip...

foreach(i; iota(10)
    .filter!(x => x%2)
    .until!(x => x>5)
    .map!"a * a"
    .filter!(x => x%4)) {
    i.writeln;
    break;
   }

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.

Collapse
 
bennypowers profile image
Benny Powers 🇮🇱🇨🇦

Thanks for sharing your view.

I have to say I disagree on your basic premise, though.

The whole purpose of methods such as map, filter and reduce is preventing the mutation of what gets passed into them.

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

let betterThings = []
for (let i = 0, i < things.length, i++) {
  const thing = things[i]
  const newThing = betterThing(thing)
  betterThings.push(newThing)
}

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.

const betterThings = things.map(betterThing)

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!

Collapse
 
beggars profile image
Dwayne Charrington • Edited

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 with Array.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:

const betterThings = [];
for (const thing of things) {
  const newThing = betterThing(thing)
  betterThings.push(newThing)
}

You could even go one step further and not bother with the newThing constant and just do this:

const betterThings = [];
for (const thing of things) {
  betterThings.push(betterThing(thing));
}

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 that map does not look cleaner and in this instance, I would also use map as well. If things was comprised of 50,000 things, I might think twice, but a few hundred or even thousand, I would stick with map.

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.

Collapse
 
bennypowers profile image
Benny Powers 🇮🇱🇨🇦

So you're saying you have a nuanced and reasonable approach to a twitter-hot-take-bait topic? That's crazy-talk! 😉

Collapse
 
ryansmith profile image
Ryan Smith • Edited

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.

  • A result set pulled from the database or API then immediately has a filter run on it. I think this is better suited for the database to do the filtering, I cannot think of a good use case for doing this unless it was not possible in SQL or in the API.
  • Converting a non-array iterable into an array for the sole purpose of using the map function on it. It seems like a wasted step when a regular loop will work on it.
  • Chaining/running all three (map, filter, and reduce) 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.
Collapse
 
beggars profile image
Dwayne Charrington

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.

A result set pulled from the database or API then immediately has a filter run on it. I think this is better suited for the database to do the filtering, I cannot think of a good use case for doing this unless it was not possible in SQL or in the API.

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.

Converting a non-array iterable into an array for the sole purpose of using the map function on it. It seems like a wasted step when a regular loop will work on it.

Oh, man, this makes me sweat profusely. I've seen this before.

Chaining/running all three (map, filter, and reduce) 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.

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.

Collapse
 
jbristow profile image
Jon Bristow • Edited

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 calling x[2] will return f(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 function f•g(x). Example: f(x)=x+2; g(x)=x / 4; h(x) = (x/4)+2 Then f(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.