Skip to main content

There is no loop

This post is really a shameless adaption of a great Mastodon post by Akshar Varma.
I’m hoping to make an excellent post even more approachable by adding some examples.

A spoon ensnared in loops.

This post will describe what could be thought of as the “evolution of the loop”, from imperative while and for loops to declarative collection functions like map and filter. Each step will take us a little bit closer to expressing what we actually mean, and away from specifying the mechanical steps of how to achieve it.

I’m going to write code which takes a list of users and creates a new array consisting of all children (all users with age less than 18). The examples will be in JavaScript, but they will be applicable to to most languages. You can think of the data as something like this.

const users = [  
    { name: "Alice", age: 10 },  
    { name: "Bob", age: 20 },  
    { name: "Charlie", age: 30 }  

Level 0: Loops not invented yet #

In prehistoric times1, programming languages had no real looping constructs. There were only comparisons and jumps (i.e. “go to”). But people realized that jumping all over the place could create some truly horrible spaghetti and that some rules were necessary. The perhaps greatest influence on this change was Edsger Wijkstra’s famous article “Go To Statement Considered Harmful”.

What came out of this was called structured programming and it introduced the idea that you could primarily do one of three things: run statements in a sequence (“normal code”), make a choice and take one of two paths (if/then/else), or repeat a block until some specific state was reached (for, while).

Level 1: while #

Thank’s to structured programming, we now have a while loop to work with!

To get the list of children we can write the following code. This is relatively straight-forward code and most programmers can probably understand what it does.

const children = [];
let i = 0;
while (i < users.length) {
    if (users[i].age < 18) {

The problem with the code is that it is very low-level. It’s just barely over assembler level. We have to write code for thing we don’t really care about, like keeping track of the indexing variable ourselves. It is easy to get lost in the details of the code and miss the actual purpose of the code.

Level 1.5: for #

As a slight improvement, we can move the indexing update out of the body of the loop.

const children = [];
for (let i = 0; i < users.length; i++) {
    if (users[i].age < 18) {

That’s a little bit better. The loop body is now free from handling the indexing variable, though we get a somewhat cryptic three-part for loop statement instead.

Some languages have range-based alternatives which makes this a bit nicer, though from our perspective it’s a little bit like putting lipstick on a pig. We still deal with an index variable. Here’s an example in Python, since JavaScript doesn’t really have this.

children = []
for i in range(len(users)):
    # ...

Level 2: for...of/in #

The next big step is realizing we never really care for the indexing variable i. It is just part of the mechanics of moving one user a a time. We can let the programming language handle that for us. We can write a loop which deals with the entities we care for (users in this example) rather than a number which we then translate to the thing we care about.

const children = [];
for (const user of users) {
    if (user.age < 18) {

Much nicer! This example uses what JavaScript calls the for...of loop. It is based on the iterator pattern and never really deals with any index variable at all. Other languages often call this something like “foreach” or “for…in” (not to be confused with JavaScript’s which iterates over object properties).

Now, this is definitely an improvement, but can we do better? Glad you asked!

Level 3: There is no spoon loop2 #

At level two, we managed to get rid of that indexing variable i which we never cared for anyway. The next step is to realize that we don’t really care for explicitly looping through anything at all!

What do we really care about? In the example, I think the important parts are “look at all users”, “keep only the children”, “child means user’s age is less than 18”. Wouldn’t it be nice if there was a construct which would let us express this an nothing else? (I think you know where I’m going with this…) Fortunately, there is!

const children = users.filter(user => user.age < 18);

Not only do we get rid of the looping construct, we also can avoid putting our children into a mutable array one at a time. As a bonus, the code now neatly fits on a singe line.

Technically, this is not a loop statement at all. It is a (higher-order) function which runs every object in the array through a given function (expressed as a lambda).

Boring loops #

When we think about it, the vast majority of loops are quite boring. They typically do one of a few things. (Or more likely, several of them mixed together.)

  • Translate objects of one type to another.
  • Filter out unwanted objects.
  • Produced a single object based on them.

If that is what we want to achieve, why not just say that? Why waste our time telling the computer how to loop through a list of objects, and putting objects into another list one at a time?

Fortunately, most programming languages now have constructs or functions in their standard library which perform these tasks.3

  • map runs each object of a collection through a function to produce a new list of other objects.
  • filter runs each object of a collection through a function to produce a list of the objects where the function returned true.
  • fold/reduce incrementally applies each object in a collection to a single new object.

There is some variation as to what other languages call these, but there is virtually always something like them. Many languages also have other similar functions, which can be very useful in specific situations. The groupBy function is my personal favorite.

Declarative over imperative #

The style we just saw in the last example, where you describe what you want to do but not how to achieve it, is called declarative. The opposite, called imperative, was used in the previous examples where we explicitly state the steps that the computer should take to achieve the result.

As a developer, I can imagine you probably want (and should) spend more time on telling the computer what to do than how to do it. So do yourself a favor and make yourself familiar with these functions if you are not already. This style is also what I argue for in Functional foundations when I talk about collection pipelines.

I should say that this post of course presents a simplified case. Not all examples can be so neatly translated to a single function, and every now and then you actually have a use for an indexing variable. But the important part of this post is to encourage you to think more declaratively.

First think about what you want to be done. Then try to express that directly (like using filter in our example). If that fails, ask a friend to help you find a way. Only if that fails too, go a head and write the imperative loop. Hopefully next time you will find a way!

  1. Structured programming was developed during the late 1950s, or soon 70 years ago. That is like a million “computer years”. ↩︎

  2. If you are puzzled by the “there is no spoon/loop” thing, it is a reference to original Matrix movie. Neo meets a monk-like boy who feeds him some Buddhist-inspired guidance: “Do not try and bend the spoon. That’s impossible. Instead… only try to realize the truth. […] Then you’ll see, that it is not the spoon that bends, it is only yourself.” Similarly, developers trained in loops may need to unlearn that very basic “truth”. ↩︎

  3. As a personal reflection on this, I would not be surprised if some programming language chose to integrate map, filter, fold, and similar functions into the actual language. It would be quite similar to how most object-oriented languages now have a “for each” / “for in” statement based on the iterator pattern. These were more convenient alternatives to the manual use of an iterator together with the traditional for loop. Or maybe not. Time will tell. ↩︎