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.
;; 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
1. Why does Clojure provide the recur special form?
2. What must be true of every use of recur?
3. What does (loop [i 0 total 0] (if (= i 5) total (recur (inc i) (+ total i)))) return?
4. What happens if a large recursive computation uses a plain self-call in tail position instead of recur?
5. When is it more idiomatic to use reduce instead of loop/recur?
Was this page helpful?
You May Also Like
Functions in Clojure
Learn how to define, call, and compose functions in Clojure, from simple defn forms to anonymous functions, multi-arity definitions, and higher-order functions.
Sequences in Clojure
Understand Clojure's sequence abstraction — the uniform interface of first, rest, and cons that lets the same functions work across lists, vectors, maps, and lazy, potentially infinite streams.
Immutable Data Structures
Explore Clojure's core persistent data structures — vectors, lists, maps, and sets — and understand how immutability and structural sharing make them both safe and efficient.
Related Reading
Related Study Notes in Programming
Browse all study notesApache Spark Study Notes
Programming · 30 topics
ProgrammingApache Flink Study Notes
Programming · 30 topics
ProgrammingHadoop Study Notes
Programming · 30 topics
ProgrammingSnowflake Study Notes
Programming · 30 topics
ProgrammingApache Airflow Study Notes
Programming · 30 topics
Programmingdbt (Data Build Tool) Study Notes
Programming · 30 topics