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
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.