Don’t pay the for-loop tax
Dan Homola
Posted on June 3, 2017
Note: this post was originally published on my Medium profile
Once, when doing code review on a TypeScript project at my work, I came across several instances when a colleague of mine used a for loop, even though it wasn’t necessary (i.e. a more readable declarative equivalent was available). In a joke I stated that we should impose a “for-loop tax for every loop used unnecessarily.
It made me think however, why so many people tend to go for the longer and more error prone solution with the loop and I got to the following conclusion: Nearly every (mainly) imperative programming language course/book I ever took/read (be it Pascal and C# in high school or C/C++ and Wolfram Mathematica in college) contained a section like
“These are the loops available, this is how you write them and this is how you use them to solve these basic problems”.
There is an important point to note here: they only teach how to write a loop but scarcely explain why you would need one (or sometimes even worse they state that loop based solutions are the best ones). For future reference, I decided to write this “cookbook of the main types of situations where loops are often used and how they can be replaced. All the examples will be written using JavaScript as it is very popular, but the rationales behind the examples can be used in many other languages as well.
#1: I need to go over an array and get a single value as a result
We start with the simplest of problems:
Given an array of numbers, return the sum of its elements.
const sum = (array) => {
let result = 0;
for (let i = 0; i < array.length; i++) {
result += array[i];
}
return result;
}
const numbers = [5, 25, 8, 18];
console.log(sum(numbers)); // logs 56
If you attended similar courses as me, you surely recognise this code. Create a temporary variable, initialise it with zero and using a for loop iterate over the array returning the final value of the variable. There are some problems though:
For something as simple as a sum of an array, 7 lines of code seem quite a lot.
You must handle the bounds of the iteration yourself. In other words you must know to start at zero (in JavaScript, many other languages have 1-based arrays – Wolfram Mathematica for example) and end at i that is strictly less than the length of the array (not less than or equal). This is prone to errors especially if your work in many languages at the same time.
const sum = (array) => array.reduce(
(total, current) => total + current,
0);
const numbers = [5, 25, 8, 18];
console.log(sum(numbers)); // logs 56
The solution that remedies both those problems is to use the reduce
function (in other languages also called fold
or aggregate
). In a single expression we iterate over each of the array elements adding them together (stating the sum’s default and initial value is zero). Notice there is no mention of the iteration bounds, it just guarantees that it will go over all the elements from first to last.
#2: I need to create a new array from a given one and transform all the elements
This is another common problem, let’s illustrate it with this example:
Given the array of prices, return a new array with the prices n % lower.
const discount = (originalPrices, discountAmount) => {
const multiplier = 1 - discountAmount;
// we must clone the array
let result = new Array(originalPrices);
for (let i = 0; i < originalPrices.length; i++) {
result[i] = originalPrices[i] * multiplier;
}
return result;
}
const prices = [5, 25, 8, 18];
console.log(discount(prices, 0.2)); //logs [ 4, 20, 6.4, 14.4 ]
The loop-based way to do this is pretty similar to the sum code. There is one additional problem though: in order to not destroy the input array, we must clone it first and then transform the values in the new array. This can easily be forgotten introducing a potentially unwanted side effect in the application.
const discount = (originalPrices, discountAmount) => {
const multiplier = 1 - discountAmount;
return originalPrices.map(price => price * multiplier);
}
const prices = [5, 25, 8, 18];
console.log(discount(prices, 0.2)); // logs [ 4, 20, 6.4, 14.4 ]
The cloning problem can be avoided altogether using the map
function. For a given array it returns a new array where each element is the corresponding element in the original array transformed using the provided function (in our case multiplied by the discount multiplier).
#3: I need the numbers from m to n
Another common situation where loops are used is when generating linear ranges as an input for further transformations. A classic example is:
Return an array of the first n squares
const squaresBad = (n) => {
let result = [];
for (let i = 1; i <= n; i++) {
result.push(i * i);
}
return result;
}
const squares = (n) => {
let result = new Array(n);
for (let i = 1; i <= n; i++) {
result[i - 1] = i * i;
}
return result;
}
console.log(squaresBad(5)); // logs [ 1, 4, 9, 16, 25 ]
console.log(squares(5)); // logs [ 1, 4, 9, 16, 25 ]
This is a problem that can be solved very badly when using loops. The first naïve solution suffers from the problem that it pushes a new element to an array every iteration. This expands the array and may cause it to reallocate in memory being slow (benchmark).
The second approach instantiates the array of correct size beforehand avoiding this problem, but we can easily make a mistake when assigning the current value (see the result[i – 1]
expression in the second for-loop).
const range = require("lodash.range")
const squaresLodash = (n) => range(1, n + 1).map(
(n) => n * n);
const squares = (n) => [...Array(n).keys()].map(
(n) => (n + 1) * (n + 1));
console.log(squaresLodash(5)); // logs [ 1, 4, 9, 16, 25 ]
console.log(squares(5)); // logs [ 1, 4, 9, 16, 25 ]
While there is no native way to generate a range of integers in JavaScript there are two ways to tackle this problem in a more declarative ways with map
: using the lodash.range
function, or a clever ES2015 syntax trick (source).
#4: I need to do something with side effects n times
The final use case of loop I want to discus here is invoking a method with side effects more than once. As Edsger Dijkstra famously said:
Two or more, use a for
The simplest example to illustrate this case is:
Console.log the string “Hello world n times
This is in my opinion the only justifiable use case for loops in JavaScript (not counting infinite loops) as it is the most concise and performant way (at least until Tail Call Optimisation arrives to most environments).
However, I would strongly recommend to abstract this into a helper function to restrict the loop to a single place.
const doNTimesLoop = (n, f) => {
for (let i = 1; i <= n; i++) {
f(i);
}
}
const doNTimesRec = (n, f) => {
const body = (m) => {
if (m > n) return;
f(m);
return body(m + 1);
}
return body(1);
}
//both log "Hello world" five times
doNTimesLoop(5, x => console.log("Hello world"));
doNTimesRec(5, x => console.log("Hello world"));
As we can see in the examples (both calling the provided function with numbers from 1 to n), the iterative version is shorter and simpler to write. Also the “loop-free version would cause a stack overflow on environments without Tail Call Optimisation.
Conclusion
On four elementary situations, we described how to use declarative style to replace loops and therefore make our code shorter and less error-prone.
Do you use loops? Do you disagree with any of the solutions? Comment please!
Posted on June 3, 2017
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.