Inductive types, recursion, structures, and type classes
Christiano Braga · Mestrado em Sistemas e Computação · IME
Based on the Hitchhiker's Guide to Logical Verification (LoVe), chapter 5.
An inductive type is its list of constructors, and every value is built by applying them, each in just one way.
For a data type, Lean generates four principles: the recursor T.rec, the non-recursive casesOn, the injectivity of each constructor, and the disjointness of distinct constructors.
Nat.succ.injEq : ∀ (u v : ℕ), (u.succ = v.succ) = (u = v)#check @Nat.succ.injEq
Nat.succ.injEq : ∀ (u v : ℕ), (u.succ = v.succ) = (u = v)The recursor is the raw form of structural recursion, and read on a proposition it is structural induction, which Lecture 7 derives.
Pattern matching and the cases tactic are surface syntax that the equation compiler and cases elaborate into the recursor.
namespace Func
def usingRec (n : ℕ) : ℕ :=
Nat.rec (motive := fun _ => ℕ)
0 (fun _ ih => ih + 2) n
example : usingRec 3 = 6 := rfl
end Func
namespace Func
example (m n : ℕ)
(h : Nat.succ m = Nat.succ n) :
m = n := m:ℕn:ℕh:m.succ = n.succ⊢ m = n
All goals completed! 🐙
example (n : ℕ) :
Nat.succ n ≠ 0 :=
Nat.succ_ne_zero n
end Func
Each recursive call is on a structurally smaller argument, so the definition terminates and elaborates to the recursor.
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
A total definition exposes its equation as a usable theorem, so accepting loopy = loopy + 1 would prove False. Lean rejects such definitions; a genuine loop needs partial def, which stays opaque.
The bogus equation gives False
namespace Func
opaque loopy : ℕ
axiom loopy_eq :
loopy = loopy + 1
theorem loopy_false : False := ⊢ False
h:loopy = loopy + 1⊢ False
All goals completed! 🐙
end Func
Rejected: no argument decreases
def loopForever {α : Type} :
List α → List α
| [] => []
| x :: xs =>
loopForever (x :: xs)
The escape hatch termination_by is for later; this lecture stays structural.
match and if bring pattern matching into a term. A match elaborates through the type's recursor, and if branches on a Decidable instance. Option packages a partial result.
namespace Func
def classify (n : ℕ) : String :=
match n with
| 0 => "zero"
| 1 => "one"
| _ => "many"
def isZero (n : ℕ) : Bool :=
if n = 0 then true else false
end Func
namespace Func
def pred? : ℕ → Option ℕ
| 0 => none
| n + 1 => some n
example : pred? 0 = none := rfl
end Func
Patterns are tried top to bottom; the wildcard _ matches anything.
A structure is a single-constructor inductive type with named fields, and Lean derives a projection for each.
namespace Func
structure Segment where
lo : ℤ
hi : ℤ
def width (s : Segment) : ℤ :=
s.hi - s.lo
def seg1 : Segment :=
{ lo := 1, hi := 5 }
end Func
namespace Func
example : seg1 = ⟨1, 5⟩ := rfl
example : width seg1 = 4 := ⊢ width seg1 = 4
All goals completed! 🐙
end Func
{ … } and ⟨…⟩ build the same value.
extends builds a larger record on a smaller one, and { r with … } copies a record changing one field.
namespace Func
structure NamedSegment
extends Segment where
name : String
def ns1 : NamedSegment :=
{ lo := 0, hi := 2, name := "a" }
end Func
namespace Func
example : ns1.lo = 0 := ⊢ ns1.lo = 0 All goals completed! 🐙
example : ns1.name = "a" := rfl
def widen (s : Segment) : Segment :=
{ s with hi := s.hi + 1 }
end Func
The inherited fields stay accessible.
A class declares operations parameterised by one or more arguments, an instance supplies them, and resolution finds the instance from those arguments. Std.Associative op is indexed by an operation, not only a type.
namespace Func
class Size (α : Type) where
size : α → ℕ
instance {α : Type} :
Size (List α) where
size xs := xs.length
instance {α : Type} :
Size (Option α) where
size
| none => 0
| some _ => 1
end Func
namespace Func
def usize {α : Type} [Size α]
(a : α) : ℕ :=
Size.size a
3#eval usize [1, 2, 3]
end Func
3
The ∈ of Lecture 2 and the associativity and commutativity of Lecture 4 are type classes resolved by type.
@Membership.mem : {α : outParam (Type u_1)} → {γ : Type u_2} → [self : Membership α γ] → γ → α → Prop#check @Membership.mem
@Std.Associative : {α : Sort u_1} → (α → α → α) → Prop#check @Std.Associative
Membership gives ∈ its meaning through an instance chosen by the type of the container, and Std.Associative and Std.Commutative are the instances ac_rfl consulted.
A tree is a leaf or a branch with a value and two subtrees; functions recurse on the subtrees.
namespace Func
inductive Tree (α : Type) where
| leaf
| branch (l : Tree α) (x : α)
(r : Tree α)
def mirror {α : Type} :
Tree α → Tree α
| .leaf => .leaf
| .branch l x r =>
.branch (mirror r) x
(mirror l)
end Func
namespace Func
def treeSize {α : Type} :
Tree α → ℕ
| .leaf => 0
| .branch l _ r =>
treeSize l + 1 + treeSize r
def t1 : Tree ℕ :=
.branch (.branch .leaf 1 .leaf)
2 .leaf
2#eval treeSize t1
end Func
2The same schema builds options, sum types, and dependent types that carry information in their own type.
namespace Func
def mapOption {α β : Type}
(f : α → β) :
Option α → Option β
| none => none
| some a => some (f a)
def fromSum : ℕ ⊕ Bool → ℕ
| .inl n => n
| .inr b => if b then 1 else 0
end Func
namespace Func
inductive Vec (α : Type) :
ℕ → Type where
| nil : Vec α 0
| cons {n : ℕ} :
α → Vec α n → Vec α (n + 1)
end Func
A Vec α n is a list of length n. The later weeks develop dependent types.
An expression is a constant, a variable, a sum, or a product, and an evaluator computes its value under an environment.
namespace Func
inductive AExp where
| const (i : ℤ)
| var (x : String)
| add (a b : AExp)
| mul (a b : AExp)
def eval (env : String → ℤ) :
AExp → ℤ
| .const i => i
| .var x => env x
| .add a b => eval env a + eval env b
| .mul a b => eval env a * eval env b
end Func
namespace Func
def sampleEnv : String → ℤ :=
fun s => if s = "x" then 3 else 0
def e1 : AExp :=
.add (.const 2)
(.mul (.var "x") (.const 5))
17#eval eval sampleEnv e1
end Func
17The syntax of a small language; its environment is the state.
One more instance extends Size to trees, and a function measures a whole list of sized values by resolution.
namespace Func
instance {α : Type} :
Size (Tree α) where
size := treeSize
def totalSize {α : Type} [Size α] :
List α → ℕ
| [] => 0
| x :: xs =>
Size.size x + totalSize xs
end Func
namespace Func
4#eval totalSize [t1, mirror t1]
end Func
4
Resolution finds the tree instance because the elements are trees, the same mechanism as Membership.
An account extends to a named account, keeping both inherited fields and adding one.
namespace Func
structure Account where
owner : String
balance : ℤ
structure NamedAccount extends Account where
nickname : String
def acc : NamedAccount :=
{ owner := "A", balance := 100, nickname := "main" }
example : acc.owner = "A" := rfl
end Func
The single constructor of a structure is the And.intro of Lecture 1, with named fields instead of positional ones.
Mirroring twice returns the original tree. On a closed tree this holds by computation.
namespace Func
example : mirror (mirror t1) = t1 := rfl
end Func
The general law mirror (mirror t) = t, for every tree, is not a computation. It needs structural induction, with the recursive calls of mirror supplying the induction hypotheses, and it is the first worked example of Lecture 7.
An inductive type is its constructors, and the kernel derives a recursor, casesOn, injectivity, and disjointness.
Structural recursion terminates, and Lean admits only terminating definitions.
match and if are pattern matching as expressions, and Option packages a partial result.
A structure is a single-constructor inductive type with named fields, built with ⟨…⟩ or { … } and extended with extends.
A type class is a structure of operations resolved by type, the mechanism behind the ∈ and the ac_rfl of the earlier lectures.
The same schema builds trees, options, sums, and dependent vectors.
Lecture 7 derives structural induction from the recursor and proves the laws stated here.
Exercises: see the lecture notes.