Loops you can read
The named let, which is a loop made of a function, and the accumulator pattern for building a list in order.
A loop that is really a function
(let loop ((i 1)) ...) gives a let a name, and that name can call the whole let again. Call it a loop if you like — it is a function that calls itself with new values, and the values it carries are the loop counters.
The advantage over a recursive walk is that nothing waits for an answer to come back. Each step passes its updated values forward and the last step answers directly. There is no pile of unfinished calls, which is why this shape can go round a great many times without trouble.
Carrying the answer with you
An accumulator is a name you carry from one step to the next, holding the answer so far. Summing the numbers up to ten means carrying a running total; collecting squares means carrying a list that grows one value at a time.
There is one wrinkle worth knowing: cons adds to the front, so a list built that way comes out backwards. The usual fix is to build the list back to front and call reverse once at the end — cheaper and clearer than reaching for the far end of the list on every step.
A loop is a function with new values
The named let is not special syntax for repetition. It builds a function, calls it once with the starting values, and the body calls the same function again with the next values. The name after let is that function’s name.
Because the call is the last thing the body does, the old values are not needed afterwards. Nothing piles up, so the loop can run thousands of times without the stack growing.
Read the header as a table: each pair is a name and its first value. On every pass, the body computes the next row of that table and hands it to the next call.
The accumulator is an invariant
An accumulator holds the answer so far. At the top of every pass, ask what it means: for a sum, it is the total of everything already seen; for a list, it is those values in reverse.
That sentence is the invariant, and it tells you both the starting value and the update. A sum starts at 0 because nothing has been seen; a product starts at 1 for the same reason.
The test decides when the whole answer is ready. (> i n) for a count from 1 to n, (null? rest) for a walk down a list. When the test passes, the accumulator is the answer, so the loop returns it unchanged.
Counting, collecting, and acting
(print (let loop ((i 1) (total 0))
(if (> i 10)
total
(loop (+ i 1) (+ total i)))))
(define (squares-up-to n)
(let loop ((i 1) (out (list)))
(if (> i n)
(reverse out)
(loop (+ i 1) (cons (* i i) out)))))
(print (squares-up-to 4))
; for-each is for the effect, not for an answer.
(for-each (lambda (w) (display w) (display " "))
(list "a" "b"))
(newline)The header is a table of names
(let loop ((i 1) (total 0)) ...) reads as two columns: a name and its first value. On each pass, the call at the bottom supplies the next row. If the order of the arguments does not match the order of the names, the values swap roles, and the program answers something wrong rather than stopping.
Give the loop a name that says what it does when the body is long enough that the call is far from the header. sum-from or walk tells the next reader what is being repeated.
Stopping is a test, not a count
The test at the top decides whether the accumulator is finished. (> i n) for a count, (null? rest) for a list, (= i 0) for a countdown. It must be checked before any work, so the empty case answers the starting accumulator rather than doing one step too many.
An off-by-one lives in that test. >= where > was meant includes one extra pass, and the output is plausible enough to miss. Print the counter on the last pass when the total looks slightly wrong.
Three loop mistakes. The loop must make progress: if the update never moves the counter toward the test, it runs forever. An off-by-one test, such as >= where you meant >, includes one extra step. And a list built with cons comes out backwards, so reverse it or count down.
Try these next
- Change the sum loop to multiply instead, starting the accumulator at 1.
- Write a loop that collects the squares of 1 to 5 in order.
- Make the loop count down from 5 and build
(5 4 3 2 1)without calling reverse.
A loop with a name
Run it and read the sum and the list. Then change the loop so it adds up the numbers from 1 to 100, and write a function that collects the odd numbers up to ten.
A product, carried forward
Run it and check that five factorial is 120. Then make `countdown-list` build `(5 4 3 2 1)` instead, and explain why no reverse is needed.
Watch the order of the arguments in (loop (+ i 1) (+ acc i)): the loop starts again from the top with those values, so the counter and the accumulator take the place of the names in the same order they were written. Swapping them is the classic first mistake, and it shows up as an answer that is wrong rather than an error.
Check what you learned
One question at a time. A wrong answer explains itself — take another go.
Part 1 of 5
What makes the named let run more than once?
