Recursion Without Growing the Continuation

Express loops as named `let` or mutually recursive procedures whose recursive calls are in documented tail positions.

See it run

LISPEX
(let loop ((xs (list 1 2 3 4)) (sum 0)) (if (null? xs) sum (loop (cdr xs) (+ sum (car xs)))))

Observed result

OUTPUT
10

Walk through the pattern

  1. Find the base case. When xs is empty, the answer already sits in sum.
  2. Shrink the remaining work. (cdr xs) removes exactly one item, so every finite proper list reaches the base case.
  3. Carry the answer forward. The next accumulator is computed before loop, leaving the recursive call as the final action.

How to reason about it

  • Carry accumulators as parameters instead of combining work after the recursive return.
  • Tail position survives final if, begin, and/or, cond, case, do, call-with-values, and apply paths.
  • Use a bounded input during testing and distinguish continuation depth from runtime cost.

Check yourself

Add 5 to the input list. What result should the program produce, and does the recursive call remain in tail position?

Answer

The result is 15. Only the input data changes; loop is still the selected branch’s final action.

A common mistake

Tail safety does not make an infinite loop terminate or remove resource budgets.

Keep going

Use the tail-call manual when converting a non-tail function, and the procedure reference when choosing a fold instead.

Proper tail calls · Higher-order procedures