100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Programming

Recursion and loop/recur

Learn how Clojure handles recursion without mutable loop variables, and how the recur special form enables safe, stack-efficient tail recursion via loop and function self-calls.

Core ConceptsIntermediate9 min readJul 10, 2026
Analogies

Recursion Without Mutation

Clojure has no for, while, or mutable loop counters in the conventional sense; iteration is expressed through recursion instead. A simple recursive function like (defn count-down [n] (when (pos? n) (println n) (count-down (dec n)))) calls itself with a smaller argument each time until the base case (pos? n) becomes false. Because Clojure's data structures are immutable, each recursive call works with its own fresh, independent value of n rather than mutating a shared counter — this makes recursive logic easier to reason about, since there is no shared state that other code could observe mid-loop.

🏏

Cricket analogy: count-down recursing with a shrinking n is like Jasprit Bumrah counting down remaining balls in a death over — 6, 5, 4... — where each ball is bowled with its own fresh mental count rather than erasing and rewriting a single shared tally mid-over.

Tail Calls and the recur Special Form

A naive recursive function that calls itself as the very last operation is called tail-recursive, but the JVM does not automatically optimize tail calls the way some languages do, so a deeply recursive function written with a plain self-call can overflow the stack. Clojure solves this with recur, a special form that must appear in tail position and jumps back to the nearest enclosing function or loop without growing the call stack. Rewriting count-down as (defn count-down [n] (when (pos? n) (println n) (recur (dec n)))) behaves identically but can safely run for millions of iterations, because the Clojure compiler turns the recur into a constant-space loop rather than a stack-consuming function call.

🏏

Cricket analogy: Using recur instead of a plain self-call is like a scorer reusing the same physical scoresheet for over after over rather than fetching a brand new sheet from a growing stack of paper for every single over bowled across a full Test match at the MCG.

loop as an Anonymous Recursion Point

recur needs somewhere to jump back to; a function's own parameter list is one such point, but loop establishes another, letting you set up local recursion without a separate named function. (loop [i 0 total 0] (if (= i 5) total (recur (inc i) (+ total i)))) initializes i to 0 and total to 0, then each recur rebinds both to new values and jumps back to the loop bindings, ultimately returning 10 (the sum of 0 through 4). This is the idiomatic way to write an accumulator-style loop in Clojure without resorting to a helper defn just to hold the recursion point.

🏏

Cricket analogy: loop with [i 0 total 0] is like a scorer setting up a fresh tally sheet at the start of an ODI innings with over-count and total-runs both starting at zero, then updating both together on the same sheet after every over via recur, rather than starting a brand new scoresheet each time.

Choosing Between recur and Higher-Order Functions

While loop/recur gives you full manual control over accumulation, idiomatic Clojure often prefers built-in higher-order functions like reduce when they fit the problem, since (reduce + 0 (range 5)) expresses the same summation as the earlier loop example far more concisely and with less room for bookkeeping mistakes. loop/recur remains the right tool when the iteration logic doesn't map cleanly onto an existing higher-order function — for example, when you need to walk two sequences at different paces, build up multiple unrelated accumulators, or implement a genuinely custom traversal, such as a recursive descent through a nested tree structure.

🏏

Cricket analogy: Preferring reduce over a manual loop when it fits is like a team using a standard run-rate calculator instead of a bowler manually tallying overs and runs by hand — but for something genuinely custom like tracking both partnership runs and balls-since-boundary together, manual scorekeeping (loop/recur) is still the right tool.

clojure
;; Naive recursion (risks StackOverflowError for large n)
(defn count-down [n]
  (when (pos? n)
    (println n)
    (count-down (dec n))))

;; Safe tail recursion with recur  constant stack space
(defn count-down-safe [n]
  (when (pos? n)
    (println n)
    (recur (dec n))))

;; loop/recur for an accumulator-style loop
(defn sum-to [n]
  (loop [i 0 total 0]
    (if (= i n)
      total
      (recur (inc i) (+ total i)))))

(sum-to 5) ;=> 10

;; The idiomatic higher-order alternative when it fits
(reduce + 0 (range 5)) ;=> 10

recur is checked at compile time: it must be in tail position (the very last expression evaluated) inside the function or loop it refers to, and it must supply exactly as many arguments as there are bindings. This static checking is what lets Clojure safely compile recur into a genuine loop instead of a stack-growing call.

Calling a function recursively without recur (a plain self-call in tail position) does not get automatic tail-call optimization on the JVM. For large inputs, this can throw a StackOverflowError even though the logic is correct — always use recur for recursive calls that could run for many iterations.

  • Clojure has no mutable loop counters; iteration is expressed through recursive function calls operating on immutable values.
  • The JVM does not automatically optimize tail calls, so recur exists to let Clojure compile tail-position recursive calls into constant-space loops.
  • recur must appear in tail position and must supply the same number of arguments as the recursion point's bindings.
  • loop establishes a local recursion point with initial bindings, letting you write accumulator-style iteration without a separate named function.
  • A plain self-call in tail position, without recur, is not automatically optimized and can overflow the stack for large inputs.
  • Idiomatic Clojure prefers built-in higher-order functions like reduce when they fit the problem, reserving loop/recur for genuinely custom traversal logic.
  • recur can target either the nearest enclosing function definition or the nearest enclosing loop form.

Practice what you learned

Was this page helpful?

Topics covered

#Programming#ClojureStudyNotes#RecursionAndLoopRecur#Recursion#Loop#Recur#Without#Loops#Algorithms#StudyNotes#SkillVeris