You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

15 KiB

+++ title = "The death of the for loop?" tags = ["code-quality", "functional"] date = "2021-03-13" author = "Braydon Kains" +++

Generally introduced to new developers around chapter 4 or 5 of their proverbial Intro to Computer Science books, loops are one of the most fundamental coding constructs a developer learns. The different simple ways we iterate over collections of data are often the core of the most complex applications ever built. This is to dramatically justify the probably-overkill rant I am about to write regarding iterating over a collection of data.

Truthfully, this title is a misnomer. I don't think for loops need to die. My goal with this post is to present a case for the available alternatives to traditional for loops. Though they aren't technically wrong, I hope to demonstrate the benefits of the alternatives and how I believe they contribute to the enhancement of code quality. (Ha, my first clickbait title. Unfortunately, this site earns me nothing. Your click paid me $0.)

I will use JavaScript for the code examples since types aren't going to be a factor here, and I feel it's the simplest language to convey the concepts in the post. I will stay as language-agnostic as possible.

The traditional for Loop

I'll start by laying out the traditional for loop everyone knows and loves. While learning the fundamentals of coding you will write your first for loops like this.

for (let i = 0; i < 10; i++) {
	// Code to execute on each iteration
}

I still think this should be the first loop a new developer learns. It's easy to understand; execute the code inside the braces 10 times. Once the developer gets to arrays, and they learn that arrays are addressed 0, 1, 2 etc. to retrieve data, the use case of for loops suddenly clicks:

const arr = [1, 2, 3, 4, 5];
for (let i = 0; i < arr.length; i++) {
	console.log(arr[i]);
}

"The loop runs from 0 to 4, so I can use i to choose an item from the array on each iteration of the loop! Now I understand what loops are for!" - A dramatic re-enactment of me getting to the array lesson in my first coding book, feeling like a genius.
It's understandable why a developer would reach for this by default; any professional developer is certain to have written hundreds of for loops exactly like this throughout their career, so there is rarely anything new for a developer to learn or understand.

Why fix what isn't broken?

foreach loops

How many times have you seen a loop like this?

const arr = [1, 2, 3, 4, 5];
for (let i = 0; i < arr.length; i++) {
	const value = arr[i];
	// do stuff with currentValue
}

This code isn't inherently wrong, but doesn't it seem like a waste to write a traditional for loop just to assign the value to a local constant variable every time? We don't actually need to modify the source collection, we just need to iterate through it and read each value individually.
Enter the foreach loop. This style of loop cuts out that boilerplate step that assigns a local constant in each iteration of your loop. Instead of each loop iteration having an index, each loop iteration will have an item. In JavaScript, this is implemented using the for...of syntax.

const arr = [1, 2, 3, 4, 5];
for (const value of arr) {
	// do stuff with value
}

In my opinion, this second option is a lot cleaner. Using this new syntax, we keep our code more concise and focused by demonstrating our intentions with the data through the syntax.
This is a microcosm of what you'll see in the rest of this post; while it's not wrong to use a traditional for loop, we should find ways to be more explicit about our intention when we iterate through a collection.

Higher-order Functions

Rather than mince words to try and explain what a Higher-order Function (hereby referred to as HOF) is, I will simply link its Wikipedia Article, as well as this chapter of Eloquent JavaScript since this article is largely in JavaScript.
Why are HOFs important to the goals of this post? This fundamental construct unlocks a number of elegant ways to use and transform collections of data with more specificity than is generally possible with native looping constructs.
Assuming that you have read the suggested articles or are already familiar with the required anonymous function syntax, let's look at some HOFs that we can use to work with collections of data. While the examples will still be in JavaScript, nearly every modern language has some version of the methods we'll discuss here.

forEach

We have learned about foreach loops, implemented in JavaScript as for...of. However, there's a HOF to do essentially the same thing. Let's restate the previous example here:

const arr = [1, 2, 3, 4, 5];
for (const value of arr) {
	// do stuff with value
}

The foreach function takes in an anonymous function with one argument that represents an individual element of the collection. So the above example could be refactored to this:

const arr = [1, 2, 3, 4, 5];
arr.forEach(
	value => {
		// do stuff with value
	}
);

When I see this function, I think "We are going to perform an operation that reads each element of the array individually" and I can focus on how we will use each element.
I started with the forEach function because it's a great introduction to HOFs in general. However, to someone not sold on HOFs as a concept, this might look no better than a for...of loop. In truth, the justification for this goes deeper into the concept of immutability and side effects that are core to the Functional Programming Manifesto. (I capitalized that like it was a real book, but unfortunately it's not. Lots of great reading if you search that exact phrase, though.)

In the interest of staying in scope for this post, let's instead move ahead to some similar HOFs that I believe provide a clear new advantage.

map

map is designed to handle the scenario where we want to apply a transformation to every element of an array. For example you may have a loop that wants to build the 2's timestable.

const arr = [1, 2, 3, 4, 5];
const twoTimestable = [];
for (const value of arr) {
	twoTimestable.push(value * 2);
}
// twoTimestable = [2, 4, 6, 8, 10]

When another coder reads this, they will be able to tell that this is a loop to build a new collection of data based on each element of a source. Using the map function, we can instead specify that a new collection is a result of transforming the source's elements individually.

const arr = [1, 2, 3, 4, 5];
const twoTimestable = arr.map(value => value * 2);
// twoTimestable = [2, 4, 6, 8, 10]

When I read the second example, I see the map function and instantly think "This is a new collection that is a transformation of the original", and I can focus on what exactly the transformation is. That's the important part anyway; the extra code that manages assigning the results to a new collection is simply boilerplate around what I would consider the unique behaviour of the program. It's what makes this program special, if you will.

filter

filter is for when we need specific data out of a collection. Let's say we want an array containing only the elements of our source that are divisible by 3.

const arr = [1, 3, 6, 8, 12];
const divisibleBy3 = [];
for (const value of arr) {
	if (value % 3 === 0) {
		divisibleBy3.push(value * 2);
	}
}
// divisibleBy3 = [3, 6, 12]

filter will give us similar benefits to map here. filter is a function that will produce a new collection that only contains the elements of source for which the specified function returns true. So the above example can be refactored to this:

const arr = [1, 3, 6, 8, 12];
const divisibleBy3 = arr.filter(value => value % 3 === 0);
// divisibleBy3 = [3, 6, 12]

When I see filter, I think "This will be a new collection of data that passes some criteria", and then I can focus on the criteria. As with map, that's what makes this program special.
In my eyes, this seems like the easiest HOF to sell. It is in my opinion the most intuitive because it's the word we would probably use in plain English to describe what we are actually trying to do.

reduce (traditionally known as fold)

This one may be the hardest to sell of the 4 HOFs we're exploring.
reduce is used for when we want to take the elements of a collection and deduce some final result from it. A good basic example would be calculating the sum of all elements in an integer array.

const arr = [1, 2, 3, 4, 5];
let sum = 0;
for (const value of arr) {
	sum += value;
}
// sum = 15 

While the idea of reduce is simple in explanation, its usage is a bit harder to wrap your head around at first. While the previous HOFs have accepted an anonymous function with a single argument (which represents the "current element" so to speak), the anonymous function we pass into reduce requires 2: the running value known as the "accumulator", and the current element (like the previous examples). This function will then return the new value for the accumulator after whatever action for the current element. In this example, the "accumulator" will be the sum we're calculating. We'll seed the accumulator with some value (in this case 0) as the second argument to the outer reduce function.

const arr = [1, 2, 3, 4, 5];
const sum = arr.reduce(
	(sum, value) => sum + value,
	0
);
// sum = 15 

Building a result that combines all the elements in a collection is a great use of reduce, but it can also be good for finding an element in a collection based on some criteria relative to the other elements in a collection. For example, if we wanted to find the max element in an array with a traditional for loop it would look something like this:

const arr = [1, 2, 5, 4, 3];
let max = Number.MIN_VALUE;
for (const value of arr) {
	if (value > max) {
		max = value;
	}
}
// max = 5

The name "accumulator" becomes a slight misnomer in this scenario, because rather than being an accumulation of all the values, it is simply the end result we are interested in. Ignoring that, the reduce we write is pretty similar to the earlier example:

const arr = [1, 2, 5, 4, 3];
const max = arr.reduce(
	(max, value) => {
		if (value > max) {
			return value;
		}
		return max;
	}
	Number.MIN_VALUE
);
// max = 5 

You might be thinking "you silly goose, this is more lines than the original for!"
Correct, I have bamboozled you to demonstrate a common gotcha for writing these HOF argument functions; they do need to return a value. The way we've been writing them (without braces) implies that the calculation is the return value of the function. However if you go to write your first reduce and wonder why the heck it's not working, the first check is to ensure that all code paths are returning a value.
This example can be written as a one-liner using a ternary expression:

const arr = [1, 2, 5, 4, 3];
const max = arr.reduce(
	(max, value) => value > max ? value : max,
	Number.MIN_VALUE
);
// max = 5 

When I see a reduce, I think "This will take the source collection and build some kind of result out of it", and I can focus on what it needs to do to find that result.

This is so sad, I love for loops. There must be some use for them!

Fear not! HOFs are awesome, and in a pure functional language like Haskell, you would only be using them at all times. However, if you are not living in the Pure Functional Utopia, there are some still some great uses for traditional for loops.

Modifying the source collection

This post has so far assumed that you are only reading from the source collection and producing a new result. I always strive not to modify values in code; it's so nice to always know with certainty what everything in your program is going to contain/equal. However, if for whatever reason you are required to modify a collection in place, a traditional index for loop is still the best way to cleanly do so.

const arr = [1, 2, 3];
for (let i = 0; i < arr.length; i++) {
	arr[i] = 69;
}
// arr = [69, 69, 69];

Comparing elements directly to previous or future elements

If you are in a scenario where you need to take different action based on elements before/after the current element, the traditional index for loop is going to be your best bet.

const arr = [1, 1, 1, 2, 2, 3];
for (let i = 0; i < arr.length; i++) {
	if (arr[i] == arr[i - 1]) {
		// do something 
	} else {
		// do something different 
	}
}

You are writing Go

Let's take a brief intermission from JavaScript. To present a new perspective.
If you're writing Go, you can probably throw everything I've said in this post out the window. The way Go handles loops is essentially the antithesis to what I've presented so far.
Not only is a for loop the only way to iterate through a collection of data, the for keyword even replaced the while keyword. This is in service of Go's language design philosophy, which is (in brief) to standardize around one way to do things as much as possible (a philosophy I'd love to rant on in a future post).
Go does have a func type, meaning one could implement their own map function like so:

func Map(arr []int, transform func(int) int) []int {
	result := make([]int, len(arr))
	for i, value := range arr {
		result[i] = transform(value)
	}
	return result
}

func main() {
	arr := []int{1, 2, 3, 4, 5}
	twoTimesTable := Map(
		arr,
		func(value int) int { return value * 2 },
	)
	// twoTimesTable = [2, 4, 6, 8, 10]
}

(I wrote this myself, but would not have been able to without this post from Algorithms to Go.)

The beauty and curse of Go is that it's up the developer to implement this if they want it. I don't know any Go developers personally, but I imagine a some would turn their nose at this while others would welcome it. If you happen to be a Go developer, I would love to hear your thoughts on this!

You are writing C

Just use for loops.

Conclusion

You may read this post and think to yourself "this guy is stupid, it's more readable to just use a for loop".
You might be right given the context of the code you're writing. "Readibility" is pretty subjective, and some people may prefer to see the boilerplate that comes with the for loop examples I presented here. My goal with this post was to present my perspective; I love the way HOFs describe explicit intentions for an iteration through a collection, allowing me to focus on the part of the code that matters.

I hope that you enjoyed reading this post! This is my first public blog post and I'm pretty nervous to show it to the world, but I really hope you gained something out of it and did not leave in an unbridled rage. Please follow my socials if you enjoyed this post and want to read more of my ramblings!