Recursion
In pure Lean code, repetition is written as recursion. Lean accepts a recursive function only when it can see that the function terminates, or when you mark it partial.
Structural Recursion
The simplest form of recursion follows the structure of the data:
1-- Recursion on Nat2def factorial : Nat → Nat3 | 0 => 14 | n + 1 => (n + 1) * factorial n56-- Recursion on List7def length : List α → Nat8 | [] => 09 | _ :: xs => 1 + length xs1011-- Recursion on custom type12inductive Tree where13 | leaf14 | node (left : Tree) (value : Nat) (right : Tree)1516def Tree.size : Tree → Nat17 | leaf => 018 | node left _ right => 1 + left.size + right.size1920#eval factorial 5 -- 12021#eval length [1, 2, 3, 4] -- 4In each case, the recursive call is on a "smaller" value—a predecessor of a Nat, the tail of a List, or a subtree. This guarantees termination.
Termination Checking
Lean requires proof that all functions terminate. For structural recursion, this is automatic. But some patterns need help:
1-- This works: clearly structural2def sum : List Nat → Nat3 | [] => 04 | x :: xs => x + sum xs56-- This needs a hint: the argument shrinks, but not structurally7def gcd (m n : Nat) : Nat :=8 if h : n = 0 then m9 else gcd n (m % n)10termination_by n11decreasing_by exact Nat.mod_lt _ (Nat.pos_of_ne_zero h)1213#eval gcd 12 18 -- 6The two clauses do different jobs, and you usually need both:
termination_by n— what gets smaller. Here, the second argument.decreasing_by …— why it gets smaller. Lean hands you a goal (m % n < n) and you prove it.
if h : n = 0 rather than if n = 0. The dependent if is what puts h : ¬n = 0 into scope in the else branch — and without it there is no way to prove m % n < n, which is false when n = 0. Forgetting the h : is the single most common reason a termination proof looks impossible.Sometimes termination is genuinely unknown to mathematics. The Collatz function is the classic example — no one has proved it terminates for every input:
1-- Honest answer: mark it partial2partial def collatz (n : Nat) : Nat :=3 if n ≤ 1 then n4 else if n % 2 == 0 then collatz (n / 2)5 else collatz (3 * n + 1)67#eval collatz 27 -- 189-- If you insist on a real def, you must admit the obligation.10-- Note "all_goals": decreasing_by gets one goal PER recursive call,11-- and a bare "sorry" would only close the first.12def collatz' (n : Nat) : Nat :=13 if n ≤ 1 then n14 else if n % 2 == 0 then collatz' (n / 2)15 else collatz' (3 * n + 1)16termination_by n17decreasing_by all_goals sorry -- warning: declaration uses 'sorry'Partial Functions
When you can't prove termination (or don't want to), use partial:
1-- Reading user input until it is valid: may never terminate,2-- and that is the intended behaviour3partial def getValidInput : IO Nat := do4 IO.println "Enter a positive number:"5 let input ← (← IO.getStdin).getLine6 match input.trimAscii.toString.toNat? with7 | some n => return n8 | none =>9 IO.println "Invalid input, try again."10 getValidInput1112-- A loop that runs until told to stop13partial def countdown (n : Nat) : IO Unit := do14 if n == 0 then15 IO.println "liftoff"16 else17 IO.println s!"{n}..."18 countdown (n - 1)1920#eval countdown 3partial is not free. Lean marks the definition opaque: it compiles and runs, but you cannot unfold it in a proof, and rfl, decide, and simp [f] will not see through it. It also requires the return type to be Inhabited, since Lean needs a value to pretend the function returns.1partial def loop (n : Nat) : Nat := if n = 0 then 0 else loop (n - 1)23#eval loop 5 -- 0, runs fine4-- example : loop 5 = 0 := rfl -- fails: loop is opaque to the kernel56-- The fuel pattern: a total function that is "partial enough".7-- Structural on the fuel argument, so Lean accepts it with no proof obligation.8def collatzFuel : Nat → Nat → Option Nat9 | 0, _ => none -- ran out of fuel10 | _, 1 => some 111 | fuel + 1, n =>12 if n % 2 == 0 then collatzFuel fuel (n / 2)13 else collatzFuel fuel (3 * n + 1)1415#eval collatzFuel 200 27 -- some 116#eval collatzFuel 5 27 -- nonesimp and rfl.partial for functions that intentionally don't terminate (servers, REPLs) or when proving termination is too complex. Regular defrequires termination proof.Tail Recursion
Tail-recursive functions are optimized to use constant stack space:
1-- NOT tail recursive: has pending work after recursive call2def factorialBad : Nat → Nat3 | 0 => 14 | n + 1 => (n + 1) * factorialBad n -- Must multiply AFTER the call56-- Tail recursive: uses an accumulator7def factorialGood (n : Nat) : Nat :=8 go n 19where10 go : Nat → Nat → Nat11 | 0, acc => acc12 | n + 1, acc => go n ((n + 1) * acc) -- Recursive call is last1314-- Both compute the same result15#eval factorialBad 10 -- 362880016#eval factorialGood 10 -- 3628800The tail-recursive version won't overflow the stack even for large inputs.
Tracing Recursive Calls
When a recursive function misbehaves, add a small trace to see the call pattern. This is a practical debugging trick.
1def traceSum : List Nat → Nat2 | [] => dbg_trace "base case"; 03 | x :: xs =>4 dbg_trace s!"visiting {x}, remaining {xs.length}"5 x + traceSum xs67#eval traceSum [1, 2, 3]8-- visiting 1, remaining 29-- visiting 2, remaining 110-- visiting 3, remaining 011-- base case12-- 6 visiting lines print before base case, because each call must recurse fully before it can do its addition. That is exactly what "not tail recursive" looks like — three pending additions stacked up. Compare this with the accumulator version in the next lesson, where the work happens on the way down.Common Recursive Patterns
Map: Transform Each Element
The map pattern applies a function to each element, building a new list with the transformed values. The recursive call processes the tail, and we prepend the transformed head.
1def map (f : α → β) : List α → List β2 | [] => []3 | x :: xs => f x :: map f xs45#eval map (· * 2) [1, 2, 3] -- [2, 4, 6]Filter: Keep Matching Elements
The filter pattern tests each element against a predicate. If the element passes, we include it in the result; otherwise, we skip it. The recursion handles the remaining elements.
1def filter (p : α → Bool) : List α → List α2 | [] => []3 | x :: xs => if p x then x :: filter p xs else filter p xs45#eval filter (· > 2) [1, 2, 3, 4, 5] -- [3, 4, 5]Fold: Accumulate a Result
The fold pattern combines all elements into a single value using an accumulator. Each step updates the accumulator with the next element until the list is exhausted.
1def foldl (f : β → α → β) (init : β) : List α → β2 | [] => init3 | x :: xs => foldl f (f init x) xs45#eval foldl (· + ·) 0 [1, 2, 3, 4, 5] -- 15Deep Dive: Why Structural vs Well-Founded Matters
The distinction is not just about which incantation you write. Lean compiles the two kinds of recursion differently, and that difference shows up in your proofs.
A structurally recursive function compiles to the type's recursor. Its defining equations hold by definition, so rfl and decide can unfold it and simp uses its equations freely.
A well-founded function (one with termination_by) compiles through WellFounded.fix. Its equations are theorems, not definitional truths, so rfl often fails where you expect it to work. You unfold with the generated f.eq_def or simp [f] instead.
1-- Structural: this holds by rfl2def len : List α → Nat3 | [] => 04 | _ :: xs => len xs + 156example : len [1, 2] = 2 := rfl -- ✓78-- Well-founded: prefer the equation lemmas over rfl9def gcd' (m n : Nat) : Nat :=10 if h : n = 0 then m else gcd' n (m % n)11termination_by n12decreasing_by exact Nat.mod_lt _ (Nat.pos_of_ne_zero h)1314example : gcd' 12 18 = 6 := by simp [gcd'] -- or: by decide, or by native_decidePractical advice: prefer structural recursion when you can restructure the function to get it, especially for anything you plan to prove things about. Reach for termination_by when the natural definition genuinely is not structural.
Mutual Recursion
Functions that call each other use mutual:
1mutual2 def isEven : Nat → Bool3 | 0 => true4 | n + 1 => isOdd n56 def isOdd : Nat → Bool7 | 0 => false8 | n + 1 => isEven n9end1011#eval isEven 4 -- true12#eval isOdd 7 -- trueRecursion vs Iteration
Lean provides for loops in the do notation, but they're syntactic sugar for recursion:
1-- Imperative-looking but actually recursive2def sumArray (arr : Array Nat) : Nat := Id.run do3 let mut total := 04 for x in arr do5 total := total + x6 return total78-- Equivalent explicit recursion9def sumArray' (arr : Array Nat) : Nat :=10 arr.foldl (· + ·) 01112#eval sumArray #[1, 2, 3, 4, 5] -- 15Implement exponentiation for natural numbers using recursion.
1def pow : Nat → Nat → Nat2 | _, 0 => 13 | x, n + 1 => x * pow x n45#eval pow 2 10 -- 1024