Formal Software Verification

6.2. Structural Recursion and Termination🔗

A function is defined by structural recursion when each recursive call is on a structurally smaller argument, one constructor closer to a base case. Such a definition terminates, and the equation compiler turns it into an application of the recursor. Factorial recurses on the predecessor, and Fibonacci has two base cases and recurses on the two predecessors.

namespace Func def fact : | 0 => 1 | n + 1 => (n + 1) * fact n def fib : | 0 => 0 | 1 => 1 | n + 2 => fib n + fib (n + 1) end Func

Lean admits as ordinary definitions only those it can show terminate, and the reason is soundness. A total definition exposes its defining equations as usable theorems and reduces during type checking, so a recursive equation such as loopy = loopy + 1, were Lean to accept it, would itself prove False. The block below posits exactly that equation as an axiom and derives the contradiction, to show what an unrestricted non-terminating definition would grant. Lean generates no such axiom, and it rejects the definitions that would produce it, which is why every function above terminates. A genuinely looping computation is still writable with partial def, but Lean then keeps the function opaque and exposes no equation, so no contradiction follows.

namespace Func opaque loopy : axiom loopy_eq : loopy = loopy + 1 theorem loopy_false : False := False h:loopy = loopy + 1False All goals completed! 🐙 end Func

For a recursion that terminates for a reason Lean cannot see structurally, termination_by with decreasing_by supplies a measure and its proof, which the guide treats later; this lecture stays within structural recursion.

6.2.1. Examples🔗

The examples below define functions by structural recursion, vary the recursive argument, and mark the definitions Lean rejects.

Example 1. Factorial and its value at 4.

namespace Func 24#eval fact 4 end Func
24

Example 2. Fibonacci needs two base cases, so its step case reads the two preceding values.

namespace Func example : fib 6 = 8 := rfl end Func

Example 3. A sum of the numbers from 0 to n, recursing on the predecessor.

namespace Func def sumTo : | 0 => 0 | n + 1 => (n + 1) + sumTo n example : sumTo 5 = 15 := rfl end Func

Example 4. The same sum with an accumulator argument, carrying the running total forwards.

namespace Func def sumAcc : | 0, acc => acc | n + 1, acc => sumAcc n (acc + (n + 1)) example : sumAcc 5 0 = 15 := rfl end Func

Example 5. power from Lecture 3 is a nested recursion, its step case calling mul on the recursive result.

example : power 2 3 = 8 := rfl

Example 6. A function may recurse on its first argument, unlike the add of Lecture 3 which recurses on its second.

namespace Func def countDown : List | 0 => [] | n + 1 => (n + 1) :: countDown n example : countDown 3 = [3, 2, 1] := rfl end Func

Example 7. Two functions may recurse through each other, declared together with mutual.

namespace Func mutual def evn : Bool | 0 => true | n + 1 => od n def od : Bool | 0 => false | n + 1 => evn n end example : evn 4 = true := rfl end Func

Example 8. A total function on Option, returning a result for both constructors.

namespace Func def orZeroList : Option (List ) List | none => [] | some xs => xs example : orZeroList none = [] := rfl end Func

Example 9. A definition Lean rejects. The recursive call is on the same list, so no argument grows smaller, Lean cannot see that it terminates, and it does not accept this equation as a total definition. The block is shown but not elaborated.

def loopForever {α : Type} : List α → List α
  | []      => []
  | x :: xs => loopForever (x :: xs)

Example 10. A base case at 0 and a step at n + 1 is the shape of every recursion on ℕ, here doubling by repeated addition.

namespace Func def twice : | 0 => 0 | n + 1 => twice n + 2 example : twice 5 = 10 := rfl end Func