Ditch for loops. Here is a case study to convince you

For loops used to be the defacto way to process collections. It's time to forget them

An Aha! moment, delivered to your inbox every week. Check out the JS Tips & Tricks Newsletter!

For loops are for machines

During my career, I’ve seen some convoluted codes, and spent debugging countless hours. A good portion of it was spent trying to figure out for loops. Since other forms of collection processing became mainstream, I hardly ever write loops anymore. And I came to the conclusion that apart from a few special cases, loops are an antipattern.

In this post, I’ll iteratively build up a solution with both a for loop and a collection pipeline. The specification is somewhat contrived, but it sheds light how loops become unmaintainable over time.

Collection processing functions

We’ll use three of them:

  • map: transforms each element and returns an Array with the results

It’s signature is (element, index, array) => newElement.

  • filter: keep only the elements that the iteratee returns truthy for

The signature is (element, index, array) => bool.

  • reduce: build up the result by repeatedly applying the iteratee to the partial result and the current element

It’s signature is (accumulator, element, index, array) => newAccumulator. We’ll only use it to sum an array of numbers: .reduce((a, e) => a + e, 0).

Scenario #1

Given an array of numbers, sum the first 10

The solutions (Try them):

With a for loop:

let result = 0;
for(let i = 0; i < 10; i++) {
	result += array[i];
}
// 55

With collection processing:

const result = array
	.filter((e, i) => i < 10) // 1, 2, ..., 10
	.reduce((a, e) => a + e, 0); // 55

So far so good, a trivial problem, trivial solutions.

Scenario #2

Given an array of numbers, sum the first 10 primes

We have an isPrime(n) => bool function ready.

The solutions (Try them):

for loop:

let result = 0;
let numPrimes = 0;
for(let i = 0; numPrimes < 10; i++) {
	const n = array[i];
	if (isPrime(n)) {
		result += n;
		numPrimes += 1;
	}
}
// 129

Collection processing:

const result = array
	.filter(isPrime) // 2, 3, 5, 7, ...
	.filter((e, i) => i < 10) // 2, 3, 5, ..., 29
	.reduce((a, e) => a + e, 0); // 129

For a localized change in the specification, the for loop solution changed in many places. Also, a new variable was needed, which is one of the worst things that hinders readability.

On the other hand, in the collection processing solution, the change was also localized.

Scenario #3

Same as before, but multiply by the distance from the previous prime

The solutions (Try them):

for loop:

let result = 0;
let numPrimes = 0;
let lastPrime = undefined;
for(let i = 0; numPrimes < 10; i++) {
	const n = array[i];
	if (isPrime(n)) {
		result += n * (lastPrime !== undefined ? n - lastPrime : 1);
		lastPrime = n;
		numPrimes += 1;
	}
}
// 471

Collection processing:

const result = array
	.filter(isPrime) // 2, 3, 5, 7, ...
	.map((e, i, a) => i > 0 ? e * (e - a[i - 1]) : e) // 2, 3, 10, 14, 44, ...
	.filter((e, i) => i < 10) // 2, 3, 10, 14, ..., 174
	.reduce((a, e) => a + e, 0); // 471

Yet again, a new variable was needed.

Scenario #4

The calculation is the same as before, but sum the last 10 numbers

The solutions (Try them):

for loop:

let lastPrimes = [];
let lastPrimeValues = [];
for(let n of array) {
	if (isPrime(n)) {
		lastPrimeValues.push(n * (lastPrimes.length > 0 ? n - lastPrimes[lastPrimes.length - 1] : 1));
		if (lastPrimeValues.length > 10) {
			lastPrimeValues.shift();
		}
		lastPrimes.push(n);
		if (lastPrimes.length > 10) {
			lastPrimes.shift();
		}
	}
}
let result = 0;
for(let n of lastPrimeValues){
	result += n;
}
// 3742

What the …!

Collection processing:

const result = array
	.filter(isPrime) // 2, 3, 5, 7, ...
	.map((e, i, a) => i > 0 ? e * (e - a[i - 1]) : e) // 2, 3, 10, 14, 44, ...
	.filter((e, i, a) => i >= a.length - 10) // 318, 354, ..., 776
	.reduce((a, e) => a + e, 0); // 3742

The first solution is convoluted, brittle, and hard to maintain, not to mention that it’s also hard to understand.

On the other hand, with collection processing, the steps are clear, easy to reason about, and short.

Conclusion

for loops are fine for simple problems, and in some extremely rare cases (much rarer than you think) when even the last drops of performance are needed. But keep in mind that due to the complexity, these solutions hardly stay performant in the long run.

Write code for humans, and not for computers. Collection processing functions make it easier. Use them, and don’t get mired in the chaos for loops tend to become.

Loops like the one above can quickly become a project’s blind spot, that nobody dares to touch.

Prefer the simple.

21 November 2017

Interesting article?

Get hand-crafted emails on new content!