3.7. Worked Examples
Each example below is carried out in full, from the choice of the equations to the checks that follow. They are disjoint from the exercises, and Lean checks every line when the notes are built.
3.7.1. Truncated subtraction
Subtraction on ℕ has no negative results, so sub 3 7 must be 0. The recursion peels one succ from each argument at once, which makes m + 1, n + 1 the recursive equation. Two base cases stop it. Subtracting zero returns the first argument, and subtracting from zero returns zero. The equations are tried in order, so the pair 0, 0 falls to the first one and never reaches the second.
def sub : ℕ → ℕ → ℕ
| m, 0 => m
| 0, _ => 0
| m + 1, n + 1 => sub m n
The first check runs the recursive equation three times, and the second exhausts the first argument before the second.
#eval sub 7 3
#eval sub 3 7
Both checks are ground equations, so each is also a theorem that rfl proves.
example : sub 3 7 = 0 := rfl
3.7.2. Evaluating an expression step by step
Take the expression (x + 3) * y of §3.2 and an environment that sends "x" to 2 and "y" to 4. Each step below applies one equation of eval, first the mul case, then the add case, then the var and num cases, and the arithmetic of ℤ finishes the computation.
eval env ((x + 3) * y) = eval env (x + 3) * eval env y = (eval env x + eval env 3) * env "y" = (env "x" + 3) * env "y" = (2 + 3) * 4 = 20
The environment is a function from names to integers, and pattern matching on strings defines it.
def workedEnv : String → ℤ
| "x" => 2
| "y" => 4
| _ => 0
Lean performs the same computation.
#eval eval workedEnv
(AExp.mul (AExp.add (AExp.var "x") (AExp.num 3))
(AExp.var "y"))
The expression contains no variables of Lean, only variable names that the environment resolves, so the equation is ground and rfl proves it.
example : eval workedEnv
(AExp.mul (AExp.add (AExp.var "x") (AExp.num 3))
(AExp.var "y")) = 20 := rfl
3.7.3. What computation settles
The function add recurses on its second argument. That single fact decides which equations rfl proves. The first equation below is ground, so both sides compute to 9. The second is general, yet add m 0 matches the first equation of add whatever m is, and reduces to m in one step.
example : add 2 7 = 9 := rfl
example (m : ℕ) : add m 0 = m := rfl
Exchanging the arguments changes everything. In add 0 m the variable sits where the recursion looks, no equation applies, and the term is stuck. The claim is true and rfl cannot prove it.
namespace Worked
theorem zero_add (m : ℕ) : add 0 m = m := m:ℕ⊢ add 0 m = m
All goals completed! 🐙
end Worked
The proof needs structural induction on m, the subject of a coming lecture. The lesson generalises. What computation settles depends on the pattern of the recursion, not on the shape of the statement.
3.7.4. From a definition to its statement
A definition usually suggests the laws it should satisfy. Appending one element at the end of a list is the mirror image of cons, so it recurses on the list and rebuilds it around the recursive call.
def snoc {α : Type} : List α → α → List α
| [], y => [y]
| x :: xs, y => x :: snoc xs y
#eval snoc [1, 2] 3
Reversal and snoc should agree: reversing x :: xs puts x at the end of the reversal of xs. Stating the law costs nothing, and the statement is what a coming lecture proves.
namespace Worked
theorem reverse_cons {α : Type} (x : α) (xs : List α) :
reverse (x :: xs) = snoc (reverse xs) x := α:Typex:αxs:List α⊢ reverse (x :: xs) = snoc (reverse xs) x
All goals completed! 🐙
end Worked
Computation does not settle it. The left side unfolds to appendPretty (reverse xs) [x], the right side is stuck on the variable list, and the two meet only under induction. Writing the statement first, and proving it later, is how a development grows.