Recursion
Recursion is the primary looping mechanism in Lean. Unlike imperative loops, recursive functions must be provably terminating.
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: n / 2 is smaller, but Lean can't see it7def collatz (n : Nat) : Nat :=8 if n ≤ 1 then n9 else if n % 2 == 0 then collatz (n / 2)10 else collatz (3 * n + 1)11termination_by n12decreasing_by sorry -- We're cheating here—Collatz termination is unproven!sorry here to skip the proof. In real code, you'd need to prove termination or use partial.Partial Functions
When you can't prove termination (or don't want to), use partial:
1-- A server loop that runs forever2partial def serverLoop (state : ServerState) : IO Unit := do3 let request ← readRequest4 let newState ← handleRequest state request5 serverLoop newState -- Infinite recursion is OK with partial67-- Reading user input until valid8partial def getValidInput : IO Nat := do9 IO.println "Enter a positive number:"10 let input ← IO.getLine11 match input.trim.toNat? with12 | some n => return n13 | none => 14 IO.println "Invalid input, try again."15 getValidInputpartial 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.
Common Recursive Patterns
Map: Transform Each Element
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
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
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: Well-Founded Recursion
Beyond structural recursion, Lean supports "well-founded recursion" where you prove that some measure decreases on each call:
1def gcd (m n : Nat) : Nat :=2 if n = 0 then m3 else gcd n (m % n)4termination_by n -- n decreases each iterationThe termination_by clause tells Lean what to track. Here,m % n is always less than n, so termination is guaranteed.
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] -- 15