h5i

Chapter 7

Inductive Types I: Natural Numbers

So far we have only universes and Π\Pi-types. They suffice for propositional logic, but mathematics also needs numbers to reason about and a way to state that two things are equal. Lean supplies both through inductive types. This chapter adds constants to the kernel, declares the natural numbers, and shows how the same mechanism supports computation and proof about them.

Constants and definitions

First we need a way to add named objects to the kernel. Terms gain constants cc:

t,A,B  ::=  ⋯  ∣  ct, A, B \;::=\; \cdots \;\mid\; c

A signature Σ\Sigma lists the constants. Each entry either declares a constant with its type, written c:Cc : C, or also gives it a body, written c:C:=tc : C \mathrel{:=} t. Entries with a body are definitions. For example, Nat:Sort 1\Nat : \Sort{1} below is a declaration, and add\add later in this chapter is a definition. Before an entry is added, its type is checked to be a type in the empty context, and its body, if any, is checked against that type, as Lean does for each def.

The signature is used in two ways. First, a constant has the type the signature gives it:

⊢Γ;ok(c:C)∈ΣΓ⊢c:C;(T-Const)\dfrac{\ctxok{\Gamma} \qquad (c : C) \in \Sigma}{\Gamma \vdash c : C}\rulename{T-Const}

where (c:C)∈Σ(c : C) \in \Sigma covers both declarations and definitions. Second, a definition can be unfolded: the constant is replaced by its body. This computation step is called delta-reduction:

c  →δ  tif (c:C:=t)∈Σ.c \;\deltared\; t \qquad \text{if } (c : C \mathrel{:=} t) \in \Sigma.

For example, add  2  1→δ(λ(m:Nat). λ(n:Nat). ⋯ )  2  1\add\;2\;1 \deltared (\tLam{m}{\Nat}{\tLam{n}{\Nat}{\cdots}})\;2\;1, after which beta-reduction continues. A declared constant without a body, such as Nat\Nat or zero\zero, never delta-reduces. Like beta, the step may be applied to any subterm.

Constants are not variables: they are never bound, so FV(c)=∅\FV(c) = \emptyset and substitution leaves them unchanged. Reduction now consists of beta, delta, and the iota-rules introduced below, and definitional equality (Chapter 12) is taken with respect to all three.

Natural numbers

In Lean, the natural numbers are declared as

inductive Nat where
  | zero : Nat
  | succ (n : Nat) : Nat

The declaration says that every natural number is built in one of two ways: it is zero\zero, or it is succ  n\suc\;n, the successor of a natural number nn. For example, 22 is succ  (succ  zero)\suc\;(\suc\;\zero). In our kernel the declaration adds three constants without bodies:

Nat:Sort 1,zero:Nat,succ:Nat→Nat.\Nat : \Sort{1}, \qquad \zero : \Nat, \qquad \suc : \Nat \to \Nat.

zero\zero and succ\suc are called constructors. They build values but never compute: succ  (succ  zero)\suc\;(\suc\;\zero) is already in normal form, and it is the number 22. We write 0,1,2,…0, 1, 2, \ldots for zero\zero, succ  zero\suc\;\zero, succ  (succ  zero)\suc\;(\suc\;\zero), and so on.

We now have numbers, but no way to compute with them: addition must distinguish zero\zero from succ  n\suc\;n, while our terms can only pass an argument along, not inspect it.

Computing: addition

How should addition be defined? By recursion on the second argument:

add  m  zero=m,add  m  (succ  k)=succ  (add  m  k).\add\;m\;\zero = m, \qquad \add\;m\;(\suc\;k) = \suc\;(\add\;m\;k).

Look at the shape of this definition. For a fixed mm, the function add  m\add\;m is determined by two pieces of data: its value on zero\zero, which is mm, and a way to turn its value rr on kk into its value on succ  k\suc\;k, which is succ  r\suc\;r. Every function f:Nat→Cf : \Nat \to C defined by recursion of this kind, called structural recursion, has the same shape:

f  zero=z,f  (succ  k)=s  k  (f  k),f\;\zero = z, \qquad f\;(\suc\;k) = s\;k\;(f\;k),

for some z:Cz : C and s:Nat→C→Cs : \Nat \to C \to C. The step function ss receives both kk and the result f  kf\;k of the recursive call. So we add one constant that builds ff from zz and ss, once and for all:

recC:C→(Nat→C→C)→Nat→C,\mathsf{rec}_C : C \to (\Nat \to C \to C) \to \Nat \to C,

together with two computation rules, called iota-rules, which are the equations above:

recC  z  s  zero  →ι  z,recC  z  s  (succ  n)  →ι  s  n  (recC  z  s  n).\mathsf{rec}_C\;z\;s\;\zero \;\iotared\; z, \qquad \mathsf{rec}_C\;z\;s\;(\suc\;n) \;\iotared\; s\;n\;(\mathsf{rec}_C\;z\;s\;n).

Addition is now an ordinary, non-recursive definition:

add:Nat→Nat→Nat:=λ(m:Nat). λ(n:Nat). recNat  m  s  n,s=λ(k:Nat). λ(r:Nat). succ  r.\add : \Nat \to \Nat \to \Nat \mathrel{:=} \tLam{m}{\Nat}{\tLam{n}{\Nat}{\mathsf{rec}_{\Nat}\;m\;s\;n}}, \qquad s = \tLam{k}{\Nat}{\tLam{r}{\Nat}{\suc\;r}}.

We write m+nm + n for add  m  n\add\;m\;n. The kernel computes 2+12 + 1 with delta, beta, and iota steps only. The explorer below is the kernel doing that, one head step at a time, with the add\add definition loaded.

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

Each row is one head step: the highlighted part is the redex, and the next row shows what it became. Change the numerals, or try add Nat.zero n with the context n : Nat to see the recursor get stuck.

Other functions follow the same pattern. Doubling, with double  (succ  k)=succ  (succ  (double  k))\mathit{double}\;(\suc\;k) = \suc\;(\suc\;(\mathit{double}\;k)), is

double:=λ(n:Nat). recNat  zero  (λ(k:Nat). λ(r:Nat). succ  (succ  r))  n,\mathit{double} \mathrel{:=} \tLam{n}{\Nat}{\mathsf{rec}_{\Nat}\;\zero\;(\tLam{k}{\Nat}{\tLam{r}{\Nat}{\suc\;(\suc\;r)}})\;n},

and double  2→succ  (succ  (double  1))→succ4  (double  0)→succ4  zero=4\mathit{double}\;2 \to \suc\;(\suc\;(\mathit{double}\;1)) \to \suc^4\;(\mathit{double}\;0) \to \suc^4\;\zero = 4.

No new recursion mechanism is needed. The constants of the inductive type, together with recC\mathsf{rec}_C and its two rules, are enough to express every structurally recursive function on Nat\Nat. And unlike general recursion, this one always terminates: each iota-step removes one succ\suc from the argument.

Proving: induction

Now consider proofs. To prove that a property P  nP\;n holds for every nn, where P:Nat→Sort 0P : \Nat \to \Sort{0}, mathematics uses induction: prove the base case P  zeroP\;\zero, and the step Π(k:Nat). P  k→P  (succ  k)\tPi{k}{\Nat}{P\;k \to P\;(\suc\;k)}, whose argument P  kP\;k is the induction hypothesis.

Through Curry-Howard, a proof of Π(n:Nat). P  n\tPi{n}{\Nat}{P\;n} is a function ff that takes nn and returns a proof of P  nP\;n. How do we build such an ff from base:P  zero\mathit{base} : P\;\zero and step\mathit{step}? For 33, the proof is step  2  (step  1  (step  0  base))\mathit{step}\;2\;(\mathit{step}\;1\;(\mathit{step}\;0\;\mathit{base})), and in general

f  zero=base,f  (succ  k)=step  k  (f  k).f\;\zero = \mathit{base}, \qquad f\;(\suc\;k) = \mathit{step}\;k\;(f\;k).

This is a structurally recursive function again, with exactly the shape of add\add above: z=basez = \mathit{base} and s=steps = \mathit{step}. Induction is recursion that returns proofs.

There is one difference: add\add has the same result type CC for every nn, whereas a proof has a type that depends on nn, such as f  zero:P  zerof\;\zero : P\;\zero and f  1:P  1f\;1 : P\;1. Because the type of recC\mathsf{rec}_C mentions a fixed CC, it cannot express induction as it stands.

One recursor for both

Putting the two cases side by side shows what to generalize.

Recursion Induction
result for nn CC P  nP\;n
case zero\zero z:Cz : C base:P  zero\mathit{base} : P\;\zero
case succ\suc s:Π(k:Nat). C→Cs : \tPi{k}{\Nat}{C \to C} step:Π(k:Nat). P  k→P  (succ  k)\mathit{step} : \tPi{k}{\Nat}{P\;k \to P\;(\suc\;k)}
result Nat→C\Nat \to C Π(n:Nat). P  n\tPi{n}{\Nat}{P\;n}

The left column is the special case of the right column in which P  nP\;n is the same type CC for every nn. So we take the right column, with PP replaced by an arbitrary function M:Nat→Sort uM : \Nat \to \Sort{u} that gives the type of the result for each nn. This function is called the motive. The result is the recursor Nat.recu\NatRec{u} of Figure 7.1. Its arguments are the motive, one minor premise for each constructor, and finally the number nn to recurse on, called the major premise. Its iota-rules are those of recC\mathsf{rec}_C. As in Lean, where the recursor is Nat.rec.{u}, there is one recursor for each universe level; since we have no universe polymorphism, the level is a subscript.

Figure 7.1. The constants for natural numbers and the iota-rules of the recursor.

Nat:Sort 1Lean: Natzero:NatNat.zerosucc:Nat→NatNat.succNat.recu:Π(M:Nat→Sort u).   M  zero→(Π(n:Nat). M  n→M  (succ  n))→Π(n:Nat). M  nNat.rec\begin{array}{lcll} \Nat &:& \Sort{1} & \text{Lean: } \texttt{Nat} \\[2pt] \zero &:& \Nat & \texttt{Nat.zero} \\[2pt] \suc &:& \Nat \to \Nat & \texttt{Nat.succ} \\[2pt] \NatRec{u} &:& \tPi{M}{\Nat \to \Sort{u}}{\;M\;\zero \to (\tPi{n}{\Nat}{M\;n \to M\;(\suc\;n)}) \to \tPi{n}{\Nat}{M\;n}} & \texttt{Nat.rec} \end{array}
Nat.recu  M  z  s  zero→ιzNat.recu  M  z  s  (succ  n)→ιs  n  (Nat.recu  M  z  s  n)\begin{array}{rcl} \NatRec{u}\;M\;z\;s\;\zero &\iotared& z \\[2pt] \NatRec{u}\;M\;z\;s\;(\suc\;n) &\iotared& s\;n\;(\NatRec{u}\;M\;z\;s\;n) \end{array}

The one recursor does both jobs, depending on the motive.

Recursion. With the constant motive λ(_:Nat). C\tLam{\_}{\Nat}{C} at level 11, the type of Nat.rec1  (λ(_:Nat). C)\NatRec{1}\;(\tLam{\_}{\Nat}{C}) is convertible to the type of recC\mathsf{rec}_C. So recC\mathsf{rec}_C is just Nat.rec1  (λ(_:Nat). C)\NatRec{1}\;(\tLam{\_}{\Nat}{C}), and from now on

add:=λ(m:Nat). λ(n:Nat). Nat.rec1  (λ(_:Nat). Nat)  m  (λ(k:Nat). λ(r:Nat). succ  r)  n,\add \mathrel{:=} \tLam{m}{\Nat}{\tLam{n}{\Nat}{\NatRec{1}\;(\tLam{\_}{\Nat}{\Nat})\;m\;(\tLam{k}{\Nat}{\tLam{r}{\Nat}{\suc\;r}})\;n}},

which is the definition in examples/zero-add.leanmini.

Induction. With a predicate M:Nat→Sort 0M : \Nat \to \Sort{0} at level 00, the type of Nat.rec0\NatRec{0} is the induction principle

M  zero→(∀n.  M  n→M  (succ  n))→∀n.  M  n.M\;\zero \to (\forall n.\; M\;n \to M\;(\suc\;n)) \to \forall n.\; M\;n.

Here the choice of imax\imax in T-Pi is essential. The conclusion Π(n:Nat). M  n\tPi{n}{\Nat}{M\;n} has type Sort imax(1,0)=Sort 0\Sort{\imax(1, 0)} = \Sort{0}, so "M  nM\;n for every nn" is a proposition although Nat\Nat lives in Sort 1\Sort{1}. With max⁡\max instead, it would live in Sort 1\Sort{1}, and statements about numbers would not be propositions at all.

The type of Nat.recu\NatRec{u} is not arbitrary: it can be read off the declaration of Nat\Nat. There is one minor premise per constructor. Each minor premise takes the constructor's arguments, plus one induction hypothesis for each argument whose type is Nat\Nat itself (a recursive argument), and returns the motive applied to the constructor. zero\zero has no arguments, so its case is just M  zeroM\;\zero; succ\suc has one recursive argument nn, so its case is Π(n:Nat). M  n→M  (succ  n)\tPi{n}{\Nat}{M\;n \to M\;(\suc\;n)}. Chapter 8 applies this recipe to more and more general declarations.

Try it

The kernel starts with Nat\Nat and its recursors already declared (Kernel::standard in src/kernel.rs). The script below defines addition and asks the kernel to confirm that 2+12 + 1 and 33 are definitionally equal, which rfl does by computing both sides. Try changing three to Nat.succ three and see the kernel refuse.

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 one : Nat := Nat.succ Nat.zero
def two : Nat := Nat.succ one
def three : Nat := Nat.succ two
# Both sides compute to succ (succ (succ zero)), so refl proves this.
goal Eq Nat (add two one) three
rfl
qed

The Eq in the goal is the equality type, declared in Chapter 9. For now, read Eq Nat a b as "a=ba = b" and rfl as "both sides compute to the same thing".

Exercises

Define multiplication mul  m  n\mathit{mul}\;m\;n by recursion on nn, using add\add, in the style of add\add above.

Solution

mul  m  zero=zero\mathit{mul}\;m\;\zero = \zero and mul  m  (succ  k)=add  m  (mul  m  k)\mathit{mul}\;m\;(\suc\;k) = \add\;m\;(\mathit{mul}\;m\;k), so mul:=λ(m:Nat). λ(n:Nat). Nat.rec1  (λ(_:Nat). Nat)  zero  (λ(k:Nat). λ(r:Nat). add  m  r)  n\mathit{mul} \mathrel{:=} \tLam{m}{\Nat}{\tLam{n}{\Nat}{\NatRec{1}\;(\tLam{\_}{\Nat}{\Nat})\;\zero\;(\tLam{k}{\Nat}{\tLam{r}{\Nat}{\add\;m\;r}})\;n}}.

def mul : Nat -> Nat -> Nat := fun (m : Nat) => fun (n : Nat) => Nat.rec1 (fun (_n : Nat) => Nat) Nat.zero (fun (k : Nat) => fun (r : Nat) => add m r) n

Why can the kernel not accept a general fixed-point combinator fix:(C→C)→C\mathsf{fix} : (C \to C) \to C with the rule fix  g→g  (fix  g)\mathsf{fix}\;g \to g\;(\mathsf{fix}\;g), which would make every recursive function definable?

Solution

With C=Π(P:Sort 0). PC = \tPi{P}{\Sort{0}}{P} and g=λ(x:C). xg = \tLam{x}{C}{x}, the term fix  g\mathsf{fix}\;g would have type Π(P:Sort 0). P\tPi{P}{\Sort{0}}{P}, a proof of everything, and the system would be inconsistent. It also reduces to itself forever, so IsDefEq\IsDefEq would not terminate. Structural recursion avoids both problems because every iota-step consumes a constructor.

What to remember

  • Constants come from a signature Σ\Sigma; definitions unfold by delta-reduction, declarations never do.
  • An inductive type adds a type former and constructors, which build values and do not compute.
  • Structural recursion on Nat\Nat is captured by one constant, the recursor, with one iota-rule per constructor.
  • Induction is the same recursor with a dependent motive M:Nat→Sort 0M : \Nat \to \Sort{0}. Recursion and induction are one operation.
  • The imax\imax rule makes "for all nn, M  nM\;n" a proposition.