Clojure from the ground up

Loop and recur

How Clojure repeats work without a loop statement: names for the values that change, and recur to start the next pass with new ones.

Part 7 of 12

Intermediate18 min readBack to Courses

There is no loop statement to write

Clojure has no for statement with a counter and an exit condition, so repetition is built from calling something again. recur is the cheap way to do it: it is not a function, it is a form that says "start this over with these new values".

It jumps back to the nearest loop with the new values in place of the old ones, and nothing is left waiting behind it. That last part is the point: the stack does not grow, so a million passes cost about what ten cost. The values still have to be passed and the stopping test still has to be written, but the cost of one more pass does not grow with how many came before.

loop gives the values names

(loop [i 1 total 0] ...) sets up two names and a body. Whatever you hand to recur becomes the next round of those names. When the body answers without calling recur, the loop is finished and that answer is the value of the whole loop.

Writing one is a small ceremony: name the state, write the body, decide when to stop. Keep recur as the very last thing in the body, because that is where Clojure can turn the call into a jump instead of a call that waits. The very last position is not a style rule: it is the one place where the runtime can see that no work remains after the jump.

When to reach for it

Most repetition in Clojure is a function over a sequence: map, filter and reduce, which arrive a few chapters from here. A loop earns its place when the next value depends on the last one in a way those functions do not say — a running total with a rule of its own, a countdown, a walk that stops early. Those are cases where the next step needs the result of the previous one; when it does not, a sequence function says the same thing with less machinery.

If you find yourself writing an index and a lookup table, ask whether a sequence function would say it better. The loop is not wrong; it is just more machinery than the job needs.

Recur rebinds, it does not call

It helps to read recur as "go round again with these values". It does not make a new call that returns to this line; it puts the new values into the loop’s names and jumps to the top. That is why the code after it never runs and why the count of passes does not grow the stack. The names are not changed in place either: each pass gets a fresh set of values, and the old ones are simply no longer reachable.

The number of values has to match the number of names exactly, in order. Two names need two values; three need three. A mismatch is caught before the loop runs, with a message about the recur not matching the loop.

The accumulator is the state you carry

A loop usually has one name for the thing being walked and one for the answer being built. The second is the accumulator: start it at the empty or zero value, and each pass adds one step’s worth. When the walk ends, the accumulator is the answer. The starting value is the answer for an empty walk, which is why it matters: zero for a sum, an empty collection for a build, and the first item for a maximum.

This shape is worth recognising because it appears everywhere: summing a column, building a string, counting matches, collecting a filtered list. Once you can write it with a loop, the sequence functions in a later chapter are a shorter spelling of the same idea.

Two counts, one shape

(defn countdown [n]
  (loop [i n]
    (when (pos? i)
      (println i)
      (recur (dec i)))))

(defn sum-to [limit]
  (loop [i 1 total 0]
    (if (> i limit)
      total
      (recur (inc i) (+ total i)))))

(countdown 3)
(println (sum-to 5))

Repeating without a repeat statement

Run it and read the three lines. Then write a `count-down-to` that prints from five down to one, and a loop that answers the product of the numbers 1 to 5.

Output
Click "Run" to execute your code

recur has to be the last thing in the loop body. Put anything after it and Clojure refuses the whole form with a message about tail position, because a call that still has work to do afterwards cannot be a jump. Let the loop answer a value and use it on the next line instead.

Three more mistakes stop a loop. The values passed to recur must match the loop’s names in number and order; a mismatch is refused before the loop runs. An accumulator that starts at the wrong value is worse than a missing one, because the loop answers confidently and wrongly. And every pass must move toward the stopping test: a state that never changes gives an endless loop, which the runner will eventually cut off rather than answer.

Try these next

  • Write a loop that adds the numbers 1 to 10 and prints the total.
  • Write a loop that builds a string of five stars, then change it to build ten.
  • Write a countdown from 3 that prints each number and then prints liftoff.

A walk that stops at one

Run it and read the three lines. Then change 6 to 7 and check the path against the step count, and add a loop that adds the numbers in a vector.

Output
Click "Run" to execute your code

Check what you learned

One question at a time. A wrong answer explains itself — take another go.

Part 1 of 5

Where does recur have to appear?