The Humble For Loop in JavaScript

I've seen some programmers try to avoid the humble for loop at all costs, in favor of more functional abstractions. I'm going to argue that the for loop is sometimes simply the best option. That doesn't mean you should always use it -- far from it -- but it does mean you should give it due consideration. The goal is to help develop some intuitions about this topic.

I talk about readability of code in this post, but I also talk about performance. My argument will be that there are cases where the for loop combines readability with performance.

I'm focusing on JavaScript as the example language, but it can stand in for many languages with similar characteristics like Python and Ruby. But the culture of JavaScript leans more towards functional programming patterns than these other languages, so I think it's the appropriate language to highlight.

Map

Functional programming patterns have been in vogue in the JavaScript world for a long time. And for good reason, as:

let result = [];
for (let entry of list) {
    result.push(transform(entry));
}

where we say we want to make a new array from an original one where each entry has been transformed using the transform function, is more involved and less declarative than:

let result = list.map(transform)

where we express the same. So using .map() is cool!

Reduce

I've run into JavaScript programmers who insist on using functional patterns in all cases, even though at the same time they struggled with implementing them. Maybe functional programming was considered a best practice by them? Perhaps being able to figure it out is considered to be a badge of being a good developer?

Let's look at reduce. .reduce(), if you're not familiar with it, takes in some collection of things and condenses it down to something else. Summing all the numbers in a list can be implemented as reduce:

let sum  = list.reduce((total, current) => total + current, 0);

This says "in order to get the sum, we add the total of everything else in the list to the value of the current item". And we start the sum with 0.

Reduce is cool, reduce is very functional, but reduce in JavaScript isn't the best fit for all purposes. Consider this one:

let flat = list_of_lists.reduce((accumulator, list) => [...accumulator, ...list], []);

This flattens a list of lists into a single list. So, [[1, 2, 3], [4, "five", 6], [7]] becomes [1, 2, 3, 4, "five", "6, 7]. 1

Let's compare it with the for loop:

let flat = [];
for (let entry of list) {
	flat.push(...entry);
}

For me, and I think for a lot of developers, the for loop is easier to understand. While I can see that for some reduce might be superior in readability, I think we all can agree that the for loop is very readable.

What is more important is that the for loop version is far more efficient. In my experiment the for loop version was about 200 times faster on a large list of about 10,000 entries. The implementation with reduce() constructs arrays all over the place, whereas the for loop version modifies the accumulator in-place. For small lists that doesn't matter much, but if you expect larger lists, it's going to matter in both processing time and memory usage. And even with very small lists the for loop version is still faster.

You could of course cheat in the reduce version and modify the accumulator in place too, just like the for loop does:

let flat = list_of_lists.reduce((accumulator, list) => {
    accumulator.push(...list);
    return accumulator;
}, []);

This code has indeed about the same performance as the for loop, but it destroys the "but this code is declarative" argument. It becomes more difficult to understand what's going on. Mutating the argument could in fact risk breaking the algorithm, and reasoning about that takes mental effort I don't want to be spending.

So, the for loop is a clear winner over this one from a readability perspective, and it's a little bit faster too, so the for loop wins.

The lesson

I think we can generalize this into a heuristic: whenever you are constructing a collection (like an array or hashmap) out of another collection, the for loop is worth your consideration, as it may have favorable trade-offs in both readability and performance. This heuristic applies to many programming languages. 2

I don't want people to take home the wrong lesson, however. What we examined here was a case where the for loop version is easy to understand and implement, and I'm arguing you should give it serious consideration in this case.

In other contexts using reduce makes complex code a lot more easy to implement and understand. And objects (when not used as a hash table) are really easy to combine, as long as you don't mutate them. Even if a for loop version were a lot faster, that very well may not be worth it in your context.

Conclusion

Consider the for loop, as in quite a few cases it results in code that's just as readable if not more so, and has better performance. The performance aspect is not surprising, given that for loops and mutability are a relatively low level constructs, and computers like those, but the readability benefits are sometimes ignored.

The intuition that for loops are sometimes plain better carries across to many other programming languages. What are the trade-offs for the humble for loop in Rust? Here is another article for your reading pleasure. You should be able to follow along even if you don't know Rust.

1

Yes, I know flat exists. I'm using flattening as an example of a task where you're constructing a structure (like an array, object or hashmap) out of another structure.

2

In a language like Haskell the culture is to use linked lists to express lists rather than arrays. These have different properties: linked lists can be combined cheaply but random access (list[i]) is linear rather than constant time, and iteration is going to somewhat more slowly than in arrays too.

This kind of easy to combine data structure is a Persistent data structure. You can use these in other languages too, including JavaScript. Objects can be used like this, as long as you don't mutate them. For lists and hashmaps using persistent data structures is far less common in JavaScript. A consideration is whether breaking with built-in data structures and cultural traditions in APIs is worth it for your purposes.

Comments

You can use your Mastodon account to reply to this post. Learn how this is implemented here.

Reply to this post

With an account on the Fediverse or Mastodon, you can respond to this post. Since Mastodon is decentralized, you can use your existing account hosted by another Mastodon server or compatible platform if you don't have an account on this one.

Copy and paste this URL into the search field of your favorite Fediverse app or the web interface of your Mastodon server.