Inductive types, recursive functions, evaluation, and the statement of theorems in Lean
Christiano Braga · Mestrado em Sistemas e Computação · IME
Based on the Hitchhiker's Guide to Logical Verification (LoVe), chapter 2.
Lecture 1 read a proof as a term whose type is the proposition it proves. The same type theory classifies data.
A type such as ℕ collects values, and a term of type ℕ → ℕ is a program. The #check below reads like the #check of a proof term, with types in place of propositions.
fun n => n + 1 : ℕ → ℕ#check fun n : ℕ => n + 1
fun n => n + 1 : ℕ → ℕThis lecture defines types and functions and states theorems about them. The proofs wait for structural induction, in the coming lectures.
An inductive command defines a type by listing its constructors. The type contains exactly the values built by finitely many constructor applications, and nothing else.
Natural numbers
namespace MyNat
inductive Nat : Type where
| zero : Nat
| succ : Nat → Nat
end MyNat
Lists over any type
namespace MyList
inductive List (α : Type) : Type where
| nil : List α
| cons : α → List α → List α
end MyList
Lean already provides Nat and List, so each reconstruction lives in a namespace.
Constructors carry data of other types. AExp is the abstract syntax of arithmetic expressions, and the imperative language of the final lectures extends it.
inductive AExp : Type where
| num : ℤ → AExp
| var : String → AExp
| add : AExp → AExp → AExp
| sub : AExp → AExp → AExp
| mul : AExp → AExp → AExp
| div : AExp → AExp → AExp
The expression (x + 3) * y
((AExp.var "x").add (AExp.num 3)).mul (AExp.var "y") : AExp#check AExp.mul
(AExp.add (AExp.var "x") (AExp.num 3))
(AExp.var "y")
((AExp.var "x").add (AExp.num 3)).mul (AExp.var "y") : AExpA function on an inductive type is defined by pattern matching, one equation per constructor shape.
Recursion is structural when each recursive call peels off one constructor. Lean accepts such definitions, since they terminate.
def add : ℕ → ℕ → ℕ
| m, Nat.zero => m
| m, Nat.succ n => Nat.succ (add m n)
def mul : ℕ → ℕ → ℕ
| _, Nat.zero => 0
| m, Nat.succ n => add m (mul m n)
def fib : ℕ → ℕ
| 0 => 0
| 1 => 1
| n + 2 => fib (n + 1) + fib n
def powerParam (m : ℕ) : ℕ → ℕ
| Nat.zero => 1
| Nat.succ n => mul m (powerParam m n)
Patterns are richer than bare constructors: n + 2 abbreviates two applications of succ. An argument that no equation inspects moves to the left of the colon, as m does above.
A definition can take a type as an argument, given explicitly at each call, or implicitly in curly braces and inferred by the elaborator. The @ prefix restores the explicit form.
Explicit type argument
def append (α : Type) : List α → List α → List α
| List.nil, ys => ys
| List.cons x xs, ys => List.cons x (append α xs ys)
append : (α : Type) → List α → List α → List α#check @append
append : (α : Type) → List α → List α → List αImplicit, with list notation
def appendPretty {α : Type} : List α → List α → List α
| [], ys => ys
| x :: xs, ys => x :: appendPretty xs ys
def reverse {α : Type} : List α → List α
| [] => []
| x :: xs => appendPretty (reverse xs) [x]
The notation writes List.nil as [] and List.cons x xs as x :: xs, so the definition reads like its own specification.
#eval runs a program through the compiler; #reduce normalizes a term symbolically in the kernel. Use #eval at scale.
An environment maps variable names to values, and eval folds an expression down to its value.
def eval (env : String → ℤ) : AExp → ℤ
| AExp.num i => i
| AExp.var x => env x
| AExp.add e₁ e₂ => eval env e₁ + eval env e₂
| AExp.sub e₁ e₂ => eval env e₁ - eval env e₂
| AExp.mul e₁ e₂ => eval env e₁ * eval env e₂
| AExp.div e₁ e₂ => eval env e₁ / eval env e₂
9#eval add 2 7
90#eval eval (fun _ => 7)
(AExp.div (AExp.var "y") (AExp.num 0))
0
Division by zero does not fail. Integer division is total, with x / 0 = 0. The command #eval and our function eval are unrelated, despite the names.
An equation whose two sides evaluate to the same value holds by rfl, the term Lecture 2 used for n * n = 9 at the witness 3.
This is definitional computation. It settles any ground equation, one without variables, and nothing more.
example : add 2 7 = 9 := rfl
example : eval (fun _ => 7)
(AExp.div (AExp.var "y") (AExp.num 0)) = 0 := rfl
In add m n = add n m the variables block computation, so the general law needs structural induction.
A theorem is a definition whose type is a proposition. Stating it requires no proof, and sorry stands where the proof will go.
namespace SorryTheorems
theorem declaration uses `sorry`add_comm (m n : ℕ) :
add m n = add n m := m:ℕn:ℕ⊢ add m n = add n m
All goals completed! 🐙
theorem declaration uses `sorry`reverse_reverse {α : Type} (xs : List α) :
reverse (reverse xs) = xs := α:Typexs:List α⊢ reverse (reverse xs) = xs
All goals completed! 🐙
end SorryTheorems
Lean flags every use of sorry
'SorryTheorems.add_comm' depends on axioms: [sorryAx]#print axioms SorryTheorems.add_comm
'SorryTheorems.add_comm' depends on axioms: [sorryAx]
#print axioms reports what a proof rests on. A proof by computation depends on no axioms at all.
An opaque constant has a type and no definition; an axiom asserts a proposition with no proof at all.
opaque a : ℤ
opaque b : ℤ
axiom a_less_b : a < b
Nothing checks an axiom, so an inconsistent one silently breaks the whole development. The course states axioms only to discuss them.
Define sub on ℕ so that sub 3 7 = 0, since ℕ has no negative values.
Choosing the equations
Recursion peels one succ from each argument, so the recursive case is m + 1, n + 1.
Two base cases stop it. Subtracting zero returns the first argument, and subtracting from zero returns zero.
The first matching equation wins, so 0, 0 falls to the first one.
def sub : ℕ → ℕ → ℕ
| m, 0 => m
| 0, _ => 0
| m + 1, n + 1 => sub m n
Checking it
4#eval sub 7 3
40#eval sub 3 7
0Each check is also a theorem, since both sides are ground.
example : sub 3 7 = 0 := rfl
(x + 3) * yUnfolding eval, one equation at a time
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
Each step is one equation of eval: the mul case, then add, then var and num.
The environment supplies the two variables, and ℤ does the rest.
The same computation in Lean
def someEnv : String → ℤ
| "x" => 2
| "y" => 4
| _ => 0
20#eval eval someEnv
(AExp.mul (AExp.add (AExp.var "x") (AExp.num 3))
(AExp.var "y"))
20example : eval someEnv
(AExp.mul (AExp.add (AExp.var "x") (AExp.num 3))
(AExp.var "y")) = 20 := rfl
Three claims about add, which recurses on its second argument.
Settled by rfl
example : add 2 7 = 9 := rfl
example (m : ℕ) : add m 0 = m := rfl
The first 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.
Not settled by rfl
namespace Worked
theorem declaration uses `sorry`zero_add (m : ℕ) : add 0 m = m := m:ℕ⊢ add 0 m = m
All goals completed! 🐙
end Worked
Here the second argument is the variable, so no equation of add applies and the term is stuck.
The claim is true, and proving it needs structural induction on m, in a coming lecture.
The pattern of the recursion, not the shape of the statement, decides which side computes.
Append one element at the end of a list, then relate it to reverse.
The definition
def snoc {α : Type} : List α → α → List α
| [], y => [y]
| x :: xs, y => x :: snoc xs y
[1, 2, 3]#eval snoc [1, 2] 3
[1, 2, 3]The statement it suggests
namespace Worked
theorem declaration uses `sorry`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
reverse (x :: xs) unfolds to appendPretty (reverse xs) [x], and snoc (reverse xs) x is stuck on the variable list, so rfl fails.
Stating a law is free; proving it is the work of the coming lectures. Writing the statement first is how a development grows.
Data and proofs share one type theory: a program is a term whose type is a function type.
inductive defines a type by its constructors; values are exactly the finite constructor applications.
Functions come from pattern matching with structural recursion, one equation per constructor shape.
Type arguments are explicit (α : Type) or implicit {α : Type}, inferred by the elaborator and restored by @.
#eval runs through the compiler, #reduce normalizes in the kernel, and rfl turns computation into a proof of any ground equation.
A theorem states a proposition; sorry defers the proof and #print axioms exposes the deferral as sorryAx.
General laws over variables need structural induction, the subject of a coming lecture.
Exercises: see the lecture notes.