Lecture 6 · Formal Software Verification

Functional Programming

Inductive types, recursion, structures, and type classes

Based on the Hitchhiker's Guide to Logical Verification (LoVe), chapter 5.

§6.1 Inductive types and their principles

  • 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.

§6.1 Recursion and case analysis from the recursor

  • 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.succm = n All goals completed! 🐙 example (n : ) : Nat.succ n 0 := Nat.succ_ne_zero n end Func

§6.2 Structural recursion

  • 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

§6.2 Why termination

  • 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 + 1False 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.

§6.3 Pattern matching expressions

  • 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.

§6.4 Structures

  • 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.

§6.4 Extending records

  • 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.

§6.5 Type classes

  • 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

§6.5 The classes we already used

  • 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.

§6.6 Binary trees

  • 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
2

§6.6 Options, sums, and dependent vectors

  • The 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.

§6.7 Worked example: arithmetic expressions

  • 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
17
  • The syntax of a small language; its environment is the state.

§6.7 Worked example: a type class for size

  • 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.

§6.7 Worked example: a record with an extension

  • 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.

§6.7 Worked example: mirroring a tree

  • 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.

Summary

  • 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.