6.7. Worked Examples
Each example below is carried out in full and verbalised. They are disjoint from the section examples and the exercises, and Lean checks every line when the notes are built.
6.7.1. Arithmetic expressions and their evaluation
An arithmetic expression is a constant, a variable, a sum, or a product, and this is an inductive type with four constructors, two of them recursive. An evaluator takes an environment giving a value to each variable and computes the value of an expression by recursion on its structure.
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
The expression below reads 2 + x × 5, and under an environment giving x the value 3 it evaluates to 17.
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))
#eval eval sampleEnv e1
end Func
The defining equation for a sum gives the law eval env (add a b) = eval env a + eval env b, and it holds by computation, since the equation is exactly the recursion step.
namespace Func
example (env : String → ℤ) (a b : AExp) :
eval env (.add a b)
= eval env a + eval env b := rfl
end Func
These expressions are the syntax of a small language, and the environment is its state. The operational semantics of the Hoare-logic weeks builds on exactly this shape.
6.7.2. A type class for size
The Size class of §6.5 extends to trees with one more instance, and a function then measures a whole list of sized values. Resolution supplies the tree instance for each element and the list structure drives the recursion.
namespace Func
instance {α : Type} : Size (Tree α) where
size := treeSize
def totalSize {α : Type} [Size α] : List α → ℕ
| [] => 0
| x :: xs => Size.size x + totalSize xs
end Func
The list below holds a tree and its mirror, each of size 2, so the total is 4.
namespace Func
#eval totalSize [t1, mirror t1]
end Func
The function requests [Size α] once, and resolution finds the tree instance because the elements are trees. The Membership instance of Lecture 2 and the associativity instance of Lecture 4 are the same mechanism seen plainly, an operation attached to a type and found by its type.
6.7.3. A record with an extension
A record collects related fields under one name, and extends builds a specialised record on a general one. An account has an owner and a balance, and a named account adds a nickname while keeping both inherited fields.
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
example : acc.balance = 100 := ⊢ acc.balance = 100 All goals completed! 🐙
end Func
Building the record with the field syntax and projecting a field returns that field, by computation. The single constructor of a structure is the same idea as the And.intro of Lecture 1, one constructor gathering several arguments, with the fields named instead of positional.
6.7.4. Mirroring a tree
The mirror of §6.6 swaps the subtrees at every branch, so mirroring twice should return 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 t, is not a computation. It needs structural induction, with the recursive calls of mirror supplying the induction hypotheses for the two subtrees, and it is the first worked example of Lecture 7. This closes the loop with Lecture 5's reverse_reverse, which proved the list analogue by recursion, and it sets up the induction to come.