6.6. Building New Datatypes
The same schema builds richer types. A binary tree is either a leaf or a branch carrying a value and two subtrees, and functions on it recurse on the subtrees. The section defines size, height, and mirror and states their laws, proving only the closed instances by computation; the general laws are Lecture 7's, since they need induction.
namespace Func
inductive Tree (α : Type) where
| leaf
| branch (l : Tree α) (x : α) (r : Tree α)
def treeSize {α : Type} : Tree α → ℕ
| .leaf => 0
| .branch l _ r => treeSize l + 1 + treeSize r
def height {α : Type} : Tree α → ℕ
| .leaf => 0
| .branch l _ r => max (height l) (height r) + 1
def mirror {α : Type} : Tree α → Tree α
| .leaf => .leaf
| .branch l x r => .branch (mirror r) x (mirror l)
end Func
The recursor of Tree shows the general schema on a fresh type. It takes a value for the leaf case and, for the branch case, a function that receives the two subtrees, the stored value, and the recursive results on the two subtrees, which become the induction hypotheses of a proof by induction.
namespace Func
#check @Tree.rec
end Func
The general laws this section states and Lecture 7 proves are mirror (mirror t) = t, treeSize (mirror t) = treeSize t, and the counting law relating the leaves and the branches of a tree. Each needs induction, so this section proves only their closed instances.
6.6.1. Examples
The examples below build a tree, compute with it, and read the other datatypes the schema produces.
Example 1. A small tree with one value at the root and one in its left subtree.
namespace Func
def t1 : Tree ℕ :=
.branch (.branch .leaf 1 .leaf) 2 .leaf
end Func
Example 2. size counts the branches, recursing on both subtrees.
namespace Func
#eval treeSize t1
end Func
Example 3. height takes the greater of the two subtree heights and adds one.
namespace Func
#eval height t1
end Func
Example 4. mirror swaps the two subtrees at every branch, and this constructor law holds for every tree by computation, with no induction.
namespace Func
example {α : Type} (l : Tree α) (x : α) (r : Tree α) :
mirror (.branch l x r)
= .branch (mirror r) x (mirror l) := rfl
end Func
The doubly-mirrored law mirror (mirror t) = t, for every tree, is different, since it needs induction, and it is a worked example of Lecture 7.
Example 5. Mirroring a leaf changes nothing.
namespace Func
example : mirror (Tree.leaf : Tree ℕ) = Tree.leaf := rfl
end Func
Example 6. Mirroring preserves the size, here on the closed tree; the general law waits for Lecture 7.
namespace Func
example : treeSize (mirror t1) = treeSize t1 := rfl
end Func
Example 7. A sum type α ⊕ β holds a value from one side or the other, and a match on inl/inr consumes it.
namespace Func
def fromSum : ℕ ⊕ Bool → ℕ
| .inl n => n
| .inr b => if b then 1 else 0
example : fromSum (.inl 4) = 4 := rfl
end Func
Example 8. Option is the canonical nullable type, and a function may map over its value. The law for some holds for every function and argument by computation, with no induction.
namespace Func
def mapOption {α β : Type} (f : α → β) :
Option α → Option β
| none => none
| some a => some (f a)
example {α β : Type} (f : α → β) (a : α) :
mapOption f (some a) = some (f a) := rfl
end Func
Example 9. A dependent inductive type carries information in its own type. A Vec α n is a list of length n, and its constructors record the length. This is a read-only preview; the later weeks develop dependent types.
namespace Func
inductive Vec (α : Type) : ℕ → Type where
| nil : Vec α 0
| cons {n : ℕ} : α → Vec α n → Vec α (n + 1)
end Func
Example 10. Because the type of vhead demands a nonempty vector, the empty case cannot arise, and the function is total without an option.
namespace Func
def vhead {α : Type} {n : ℕ} : Vec α (n + 1) → α
| .cons x _ => x
def v1 : Vec ℕ 2 := .cons 3 (.cons 4 .nil)
#eval vhead v1
end Func