To state facts about numbers, such as 0+n=n, we need a way to express equality. Although it might seem to require special rules built into the kernel, equality is another inductive type generated by the recipe of Chapter 8. Its recursor lets us turn a fact about a into a fact about b. This chapter declares the type, explains how it differs from definitional equality, and proves the book's first substantial theorem.
The declaration
In Lean, equality is declared as
inductive Eq {A : Sort u} (a : A) : A -> Propwhere
| refl : Eq a a
For a,b:A, the type EquAab is the proposition that a and b are equal; when A is Nat we write it a=b. In the terms of Chapter 8, A and a are parameters and b is an index:
The only constructor reflu has no fields, and its index term is a: it can only prove equations whose two sides are the samea. So Eq is an inductive family like Vec, not a single type. The left side is fixed by the parameter; the right side varies, and the constructor pins it to the left side.
The recursor: transport
Informally, the recursor of Eq says: if a=b, then whatever holds of a holds of b. This is Leibniz's principle, and in its most common form it reads
Given a property M of elements of A, evidence m:Ma, and a proof h that a=b, it returns evidence of Mb. We say that it transportsm along h. Its computation rule says that transporting along refl does nothing:
Eq.ndrecu,vAaMmb(refluAc)→m.
The version the general recipe of Chapter 8 generates is slightly more general, and Eq.ndrec is a definition built from it.
the full dependent recursor, as generated by the recipe
Because b is an index, the motive takes band the proof h:EquAab, and there is one minor premise, for reflu, with no fields and no hypotheses:
Eq is a subsingleton (one constructor, no fields), so v is arbitrary: equality proofs can be used to build data, not only proofs. The non-dependent Eq.ndrecu,v is the special case whose motive ignores the proof:
In a well-typed redex b and c are both convertible to a, so the rules can ignore them. Figure 9.1 summarizes the constants.
Figure 9.1. The constants for equality and their computation rules. The rule for Eq.ndrecu,v follows from its definition by delta, beta, and the rule for Eq.recu,v.
For example, suppose P:Nat→Sort0, a,b:Nat, h:a=b, and pa:Pa. Choosing the motive M=P gives
Eq.ndrec1,0NataPpabh:Pb.
This one line is the logical content of rewriting: a fact about a becomes a fact about b. The rw tactic of Chapter 10 builds such terms automatically.
Definitional versus propositional equality
There are now two notions of equality, and keeping them apart is essential to understanding how a proof assistant behaves.
Definitional equality≡ is decided by the kernel, by computation. It is a judgment about terms, not a type; no proof object is involved.
Propositional equalityEq is a type. Its proofs are terms, and finding one may require real reasoning, such as induction.
The constructor refl connects them: by T-Conv, refluAa:EquAab whenever a≡b. So every definitional equality yields a propositional one for free. The converse fails, and the standard example is the two halves of n+0=n and 0+n=n. With the definition of add from Chapter 7 and s its step function,
n+0→δβNat.rec1(λ(_:Nat).Nat)nszero→ιn,
so refl1Natn:n+0=n, and Lean proves this by rfl. In contrast,
0+n→δβNat.rec1(λ(_:Nat).Nat)0sn
is stuck: n is a variable, and the iota-rules fire only on zero and succ. So 0+n=n is not provable by refl and needs induction.
The reduction explorer shows both computations with n as a variable in the context. The first reaches n; the second stops with the recursor waiting for a constructor it will never get.
def add : Nat -> Nat -> Nat := fun (m : Nat) => fun (n : Nat) => Nat.rec1 (fun (_n : Nat) => Nat) m (fun (k : Nat) => fun (r : Nat) => Nat.succ r) n
def add : Nat -> Nat -> Nat := fun (m : Nat) => fun (n : Nat) => Nat.rec1 (fun (_n : Nat) => Nat) m (fun (k : Nat) => fun (r : Nat) => Nat.succ r) n
def add : Nat -> Nat -> Nat := fun (m : Nat) => fun (n : Nat) => Nat.rec1 (fun (_n : Nat) => Nat) m (fun (k : Nat) => fun (r : Nat) => Nat.succ r) n
# n + 0 = n holds by computation: the recursor sees the constructor zero.
goal forall (n : Nat), Eq Nat (add n Nat.zero) n
intro n
rfl
qed
# 0 + n = n does not: the recursor is stuck on the variable n.
goal forall (n : Nat), Eq Nat (add Nat.zero n) n
intro n
rfl
Example: proving 0 + n = n
We prove Π(n:Nat).0+n=n by induction, using Nat.rec0 with the motive M0=λ(n:Nat).0+n=n.
Base. We need M00, which is convertible to 0+0=0. Since 0+0 computes to 0, the term refl1Nat0 works.
Step. Given k:Nat and ih:0+k=k, we need M0(succk), that is, 0+succk=succk. By the second iota-rule, 0+succk computes to succ(0+k), so it suffices to prove succ(0+k)=succk. We start from refl1Nat(succ(0+k)), a proof of succ(0+k)=succ(0+k), and transport it along ih with the motive N=λ(b:Nat).succ(0+k)=succb, which replaces the second0+k by k:
The kernel checks it with Algorithm 5.1. Each argument is checked against the corresponding part of the type of Nat.rec0, instantiated with M0, and every comparison succeeds after normalization. Here is the complete term, submitted to the kernel in one piece. The non-dependent recursor is defined first, as in Figure 9.1.
def add : Nat -> Nat -> Nat := fun (m : Nat) => fun (n : Nat) => Nat.rec1 (fun (_n : Nat) => Nat) m (fun (k : Nat) => fun (r : Nat) => Nat.succ r) n
# Eq.ndrec at levels (1, 0): transport a proof of M a along a = b to M b.
def ndrec : forall (A : Sort 1), forall (a : A), forall (M : A -> Sort 0), M a -> forall (b : A), Eq A a b -> M b := fun (A : Sort 1) => fun (a : A) => fun (M : A -> Sort 0) => fun (m : M a) => fun (b : A) => fun (h : Eq A a b) => Eq.rec0 A a (fun (b' : A) => fun (_h : Eq A a b') => M b') m b h
goal forall (n : Nat), Eq Nat (add Nat.zero n) n
exact Nat.rec0 (fun (n : Nat) => Eq Nat (add Nat.zero n) n) (Eq.refl Nat Nat.zero) (fun (k : Nat) => fun (ih : Eq Nat (add Nat.zero k) k) => ndrec Nat (add Nat.zero k) (fun (b : Nat) => Eq Nat (Nat.succ (add Nat.zero k)) (Nat.succ b)) (Eq.refl Nat (Nat.succ (add Nat.zero k))) k ih)
qed
Writing such terms by hand is tedious, and finding the right motive is the hard part: the motive N had to mention b in exactly one of the two places where 0+k occurs. Tactics (Chapter 10) automate both the bookkeeping and the choice of motive.
Changes to the kernel
Little changed to get from propositional logic to arithmetic with equality. Infer gained a case for constants, which looks up the type in Σ (T-Const). Check is unchanged. Only Whnf, and through it IsDefEq, learned the new delta- and iota-steps. Chapter 12 shows how a single Whnf handles every inductive type, and Chapter 14 discusses what inductive types change in the metatheory.
Exercises
Prove symmetry of equality: build a term of type Π(a:Nat).Π(b:Nat).a=b→b=a using Eq.ndrec.
Solution
Transport the proof refla:a=a along h:a=b with the motive λ(x:Nat).x=a, which turns a=a into b=a:
λ(a:Nat).λ(b:Nat).λ(h:a=b).Eq.ndrec1,0Nata(λ(x:Nat).x=a)(refl1Nata)bh.
goalforall (a : Nat), forall (b : Nat), Eq Nat a b -> Eq Nat b a
exactfun (a : Nat) => fun (b : Nat) => fun (h : Eq Nat a b) => ndrec Nat a (fun (x : Nat) => Eq Nat x a) (Eq.refl Nat a) b h
qed
(with ndrec defined as in the playground above).
Explain why Eq eliminates into every universe although it is a proposition, but a type Or with two constructors does not.
Solution
Eq is a subsingleton: one constructor with no fields. A function defined by its recursor cannot distinguish two proofs of a=b, because there is only one way to build any such proof. Or has two constructors, so its recursor into Nat could return different numbers for the two injections, making the value of a program depend on which proof was supplied. Chapter 8 states the rule precisely.
What to remember
Eq is an inductive family with parameters A and a, index b, and one constructor refl with no fields.
Its recursor transports a fact about a to a fact about b along a proof of a=b, and does nothing on refl. This is rewriting.
Definitional equality is decided by computation; propositional equality is a type whose proofs may need induction. refl turns the former into the latter.
n+0=n is definitional and 0+n=n is not, because addition recurses on its second argument.