h5i

Chapter 8

Inductive Types II: The General Recipe

Chapter 7 built the recursor for the natural numbers by hand. In Lean, a user never does that: they write an inductive declaration, and the kernel checks it and generates the type former, the constructors, the recursor, and its computation rules. This chapter develops that procedure in three steps, from more constructors to parameters to indices, and then states it in general. The kernel's own code for it is about five hundred lines.

More constructors: trees

Nat\Nat has two constructors, and only one of them has a recursive argument. Consider a type with three:

inductive Tree where
  | leaf   : Tree
  | node   : Tree -> Tree
  | branch : Tree -> Tree -> Tree

For example, branch  (node  leaf)  leaf\mathsf{branch}\;(\mathsf{node}\;\mathsf{leaf})\;\mathsf{leaf} is a tree built from four constructors. To build M  tM\;t for every tree tt, where M:Tree→Sort vM : \mathsf{Tree} \to \Sort{v}, the recipe of Chapter 7 asks for one case per constructor, and each argument of type Tree\mathsf{Tree} contributes one induction hypothesis:

mleaf:M  leafmnode:Π(t:Tree). M  t→M  (node  t)mbranch:Π(l:Tree). Π(r:Tree). M  l→M  r→M  (branch  l  r)\begin{array}{lcl} m_{\mathsf{leaf}} &:& M\;\mathsf{leaf} \\[2pt] m_{\mathsf{node}} &:& \tPi{t}{\mathsf{Tree}}{M\;t \to M\;(\mathsf{node}\;t)} \\[2pt] m_{\mathsf{branch}} &:& \tPi{l}{\mathsf{Tree}}{\tPi{r}{\mathsf{Tree}}{M\;l \to M\;r \to M\;(\mathsf{branch}\;l\;r)}} \end{array}

The recursor takes the motive, the three cases, and a tree:

Tree.recv:Π(M:Tree→Sort v).   M  leaf→(⋯node)→(⋯branch)→Π(t:Tree). M  t,\mathsf{Tree.rec}_v : \tPi{M}{\mathsf{Tree} \to \Sort{v}}{\; M\;\mathsf{leaf} \to (\cdots_{\mathsf{node}}) \to (\cdots_{\mathsf{branch}}) \to \tPi{t}{\mathsf{Tree}}{M\;t}},

where the dots stand for the types of mnodem_{\mathsf{node}} and mbranchm_{\mathsf{branch}}. Compared with Nat.recv\NatRec{v}, only the number of cases and the number of hypotheses per case have changed. Writing RR for the partial application Tree.recv  M  mleaf  mnode  mbranch\mathsf{Tree.rec}_v\;M\;m_{\mathsf{leaf}}\;m_{\mathsf{node}}\;m_{\mathsf{branch}}, the iota-rules are

R  leaf→ιmleafR  (node  t)→ιmnode  t  (R  t)R  (branch  l  r)→ιmbranch  l  r  (R  l)  (R  r).\begin{array}{rcl} R\;\mathsf{leaf} &\iotared& m_{\mathsf{leaf}} \\[2pt] R\;(\mathsf{node}\;t) &\iotared& m_{\mathsf{node}}\;t\;(R\;t) \\[2pt] R\;(\mathsf{branch}\;l\;r) &\iotared& m_{\mathsf{branch}}\;l\;r\;(R\;l)\;(R\;r). \end{array}

Each rule selects the case for the constructor at the head of the major premise, and fills each hypothesis with a recursive call on the corresponding subtree. For example, with M=λ(_:Tree). NatM = \tLam{\_}{\mathsf{Tree}}{\Nat} and the cases

mleaf=1,mnode=λ(t:Tree). λ(i:Nat). 1+i,mbranch=λ(l:Tree). λ(r:Tree). λ(i:Nat). λ(j:Nat). 1+i+j,m_{\mathsf{leaf}} = 1, \qquad m_{\mathsf{node}} = \tLam{t}{\mathsf{Tree}}{\tLam{i}{\Nat}{1 + i}}, \qquad m_{\mathsf{branch}} = \tLam{l}{\mathsf{Tree}}{\tLam{r}{\mathsf{Tree}}{\tLam{i}{\Nat}{\tLam{j}{\Nat}{1 + i + j}}}},

RR counts the constructors of a tree:

R  (branch  (node  leaf)  leaf)→1+R  (node  leaf)+R  leaf→1+(1+R  leaf)+1→1+(1+1)+1=4.R\;(\mathsf{branch}\;(\mathsf{node}\;\mathsf{leaf})\;\mathsf{leaf}) \to 1 + R\;(\mathsf{node}\;\mathsf{leaf}) + R\;\mathsf{leaf} \to 1 + (1 + R\;\mathsf{leaf}) + 1 \to 1 + (1 + 1) + 1 = 4.

With MM a predicate (v=0v = 0), the same recursor is structural induction on trees. In general, a type with nn constructors gets a recursor with nn minor premises m1,…,mnm_1, \ldots, m_n, and the kernel needs no code specific to the type: it only needs to know, for each constructor, which case to pick and which arguments to recurse on.

Here is that tree as the kernel sees it. Each box is a constructor application; the recursive fields hang below it, and the line at the bottom of each box is the recursor case that handles it, with (R  l)(R\;l) and (R  r)(R\;r) the induction hypotheses computed by recursive calls. Type another tree to see its cases.

inductive Tree : Sort 1 where
  | leaf
  | node (t : Tree)
  | branch (l : Tree) (r : Tree)
inductive Tree : Sort 1 where
  | leaf
  | node (t : Tree)
  | branch (l : Tree) (r : Tree)

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 four : Nat := Nat.succ (Nat.succ (Nat.succ one))

# The minor premise for branch takes l, r, then the two induction hypotheses.
def count : Tree -> Nat := fun (t : Tree) => Tree.rec1 (fun (_t : Tree) => Nat) one (fun (t : Tree) => fun (i : Nat) => Nat.succ i) (fun (l : Tree) => fun (r : Tree) => fun (i : Nat) => fun (j : Nat) => Nat.succ (add i j)) t

goal Eq Nat (count (Tree.branch (Tree.node Tree.leaf) Tree.leaf)) four
rfl
qed

Parameters: lists

A list of numbers is either empty or a number in front of a list of numbers, so we could declare a type ListNat\mathsf{ListNat} with constructors nil\mathsf{nil} and cons:Nat→ListNat→ListNat\mathsf{cons} : \Nat \to \mathsf{ListNat} \to \mathsf{ListNat}. But then lists of trees would need another declaration, ListTree\mathsf{ListTree}, with the same constructors and a separate recursor, and so on for every element type. It is better to make the element type an argument of the type itself:

inductive List (A : Sort 1) where
  | nil  : List A
  | cons : A -> List A -> List A

Now List  Nat\mathsf{List}\;\Nat and List  Tree\mathsf{List}\;\mathsf{Tree} are two instances of one declaration. The declaration adds

List:Sort 1→Sort 1,nil:Π(A:Sort 1). List  A,cons:Π(A:Sort 1). A→List  A→List  A.\mathsf{List} : \Sort{1} \to \Sort{1}, \qquad \mathsf{nil} : \tPi{A}{\Sort{1}}{\mathsf{List}\;A}, \qquad \mathsf{cons} : \tPi{A}{\Sort{1}}{A \to \mathsf{List}\;A \to \mathsf{List}\;A}.

The constructors take AA as an explicit first argument. For example, the one-element list [5][5] is cons  Nat  5  (nil  Nat)\mathsf{cons}\;\Nat\;5\;(\mathsf{nil}\;\Nat). Within one list, AA never changes: the tail of cons  A  a  as\mathsf{cons}\;A\;a\;\mathit{as} is again a List  A\mathsf{List}\;A. An argument of the type that stays the same in every constructor and every recursive argument is called a parameter. Because it never changes, the recursor can take it once, before the motive, and treat it as fixed:

List.recv:Π(A:Sort 1). Π(M:List  A→Sort v).   M  (nil  A)→(Π(a:A). Π(as:List  A). M  as→M  (cons  A  a  as))→Π(x:List  A). M  xList.recv  A  M  m1  m2  (nil  A′)  →ι  m1List.recv  A  M  m1  m2  (cons  A′  a  as)  →ι  m2  a  as  (List.recv  A  M  m1  m2  as)\begin{array}{l} \mathsf{List.rec}_v : \tPi{A}{\Sort{1}}{\tPi{M}{\mathsf{List}\;A \to \Sort{v}}{\;M\;(\mathsf{nil}\;A) \to {}}} \\[2pt] \qquad (\tPi{a}{A}{\tPi{\mathit{as}}{\mathsf{List}\;A}{M\;\mathit{as} \to M\;(\mathsf{cons}\;A\;a\;\mathit{as})}}) \to \tPi{x}{\mathsf{List}\;A}{M\;x} \\[8pt] \mathsf{List.rec}_v\;A\;M\;m_1\;m_2\;(\mathsf{nil}\;A') \;\iotared\; m_1 \\[2pt] \mathsf{List.rec}_v\;A\;M\;m_1\;m_2\;(\mathsf{cons}\;A'\;a\;\mathit{as}) \;\iotared\; m_2\;a\;\mathit{as}\;(\mathsf{List.rec}_v\;A\;M\;m_1\;m_2\;\mathit{as}) \end{array}

Apart from the leading AA, this is the recipe of Chapter 7 once more: cons\mathsf{cons} has two arguments, of which only as\mathit{as} is recursive, so its case gets one induction hypothesis. The rules ignore the parameter A′A' stored in the constructor; in a well-typed term it is convertible to AA. For example, the length of a list is

length:=λ(A:Sort 1). λ(x:List  A). List.rec1  A  (λ(_:List  A). Nat)  zero  (λ(a:A). λ(as:List  A). λ(r:Nat). succ  r)  x.\mathit{length} \mathrel{:=} \tLam{A}{\Sort{1}}{\tLam{x}{\mathsf{List}\;A}{ \mathsf{List.rec}_1\;A\;(\tLam{\_}{\mathsf{List}\;A}{\Nat})\;\zero\;(\tLam{a}{A}{\tLam{\mathit{as}}{\mathsf{List}\;A}{\tLam{r}{\Nat}{\suc\;r}}})\;x}}.

A list value carries the parameter AA in every constructor, which the tree below shows next to each box: the parameter is repeated, the fields differ.

inductive List (A : Sort 1) : Sort 1 where
  | nil
  | cons (head : A) (tail : List A)
inductive List (A : Sort 1) : Sort 1 where
  | nil
  | cons (head : A) (tail : List A)

def length : forall (A : Sort 1), List A -> Nat := fun (A : Sort 1) => fun (xs : List A) => List.rec1 A (fun (_xs : List A) => Nat) Nat.zero (fun (head : A) => fun (tail : List A) => fun (r : Nat) => Nat.succ r) xs

# length [0, 0] = 2
goal Eq Nat (length Nat (List.cons Nat Nat.zero (List.cons Nat Nat.zero (List.nil Nat)))) (Nat.succ (Nat.succ Nat.zero))
rfl
qed

Indices: vectors

A List  Nat\mathsf{List}\;\Nat may have any length. What if we want the length to be part of the type, so that Vec  Nat  0\mathsf{Vec}\;\Nat\;0 and Vec  Nat  2\mathsf{Vec}\;\Nat\;2 are different types?

inductive Vec (A : Sort 1) : Nat -> Sort 1 where
  | vnil  : Vec A 0
  | vcons : (n : Nat) -> A -> Vec A n -> Vec A (succ n)

The element type AA is still a parameter. The length is not: vnil\mathsf{vnil} has length 00, and vcons\mathsf{vcons} takes a vector of length nn and returns one of length succ  n\suc\;n. An argument of the type that may differ between constructors, and between a constructor and its recursive arguments, is called an index. The constructors are

vnil:Π(A:Sort 1). Vec  A  zero,vcons:Π(A:Sort 1). Π(n:Nat). A→Vec  A  n→Vec  A  (succ  n).\mathsf{vnil} : \tPi{A}{\Sort{1}}{\mathsf{Vec}\;A\;\zero}, \qquad \mathsf{vcons} : \tPi{A}{\Sort{1}}{\tPi{n}{\Nat}{A \to \mathsf{Vec}\;A\;n \to \mathsf{Vec}\;A\;(\suc\;n)}}.

Because an index varies, the recursor cannot fix it in advance. Instead, the motive takes it as an argument, M:Π(n:Nat). Vec  A  n→Sort vM : \tPi{n}{\Nat}{\mathsf{Vec}\;A\;n \to \Sort{v}}, and the case for vcons\mathsf{vcons} is

Π(n:Nat). Π(a:A). Π(xs:Vec  A  n). M  n  xs→M  (succ  n)  (vcons  A  n  a  xs).\tPi{n}{\Nat}{\tPi{a}{A}{\tPi{\mathit{xs}}{\mathsf{Vec}\;A\;n}{M\;n\;\mathit{xs} \to M\;(\suc\;n)\;(\mathsf{vcons}\;A\;n\;a\;\mathit{xs})}}}.

The induction hypothesis is about a vector of length nn, read off the type Vec  A  n\mathsf{Vec}\;A\;n of the recursive argument, and the conclusion is about one of length succ  n\suc\;n, read off the result type of the constructor. The script examples/generic-vec.leanmini (Appendix E) declares this type and checks a small term that uses it.

The general recipe

This section states the procedure for a single inductive type with parameters and indices. Mutual and nested inductive types, which Lean also supports, are out of scope.

The procedure also answers a question that T-Const leaves open. A constant without a body is an assumption: T-Const would accept c:Π(P:Sort 0). Pc : \tPi{P}{\Sort{0}}{P} just as readily as zero:Nat\zero : \Nat, and with it the system would be inconsistent. The constants generated below are not arbitrary assumptions. They are instances of a fixed schema, the inductive types of the Calculus of Inductive Constructions, whose consistency is known [Werner 1994].

Telescopes. We write (x⃗:A⃗)(\vec{x} : \vec{A}) for a sequence of bindings (x1:A1)⋯(xk:Ak)(x_1 : A_1) \cdots (x_k : A_k) in which each AiA_i may mention x1,…,xi−1x_1, \ldots, x_{i-1}; Π(x⃗:A⃗). B\tPi{\vec{x}}{\vec{A}}{B} for Π(x1:A1). ⋯ Π(xk:Ak). B\tPi{x_1}{A_1}{\cdots\,\tPi{x_k}{A_k}{B}}; and f  x⃗f\;\vec{x} for f  x1⋯xkf\;x_1 \cdots x_k. The length of x⃗\vec{x} is ∣x⃗∣|\vec{x}|. In the code, a telescope is a Context, and telescope(binders, tail) in src/term.rs builds the nested Π\Pi.

Declarations. A declaration introduces a type former II and constructors c1,…,cnc_1, \ldots, c_n:

I:Π(p⃗:P⃗). Π(ı⃗:J⃗). Sort ucj:Π(p⃗:P⃗). Π(a⃗:A⃗j). I  p⃗  t⃗j(j=1,…,n)\begin{array}{rcll} I &:& \tPi{\vec{p}}{\vec{P}}{\tPi{\vec{\imath}}{\vec{J}}{\Sort{u}}} & \\[2pt] c_j &:& \tPi{\vec{p}}{\vec{P}}{\tPi{\vec{a}}{\vec{A}_j}{I\;\vec{p}\;\vec{t}_j}} & (j = 1, \ldots, n) \end{array}

The parameters p⃗\vec{p} are shared by all constructors and appear unchanged in every result type. The indices ı⃗\vec{\imath} may differ between constructors: the index terms t⃗j\vec{t}_j may mention p⃗\vec{p} and the constructor's own arguments a⃗\vec{a}, which are called its fields. For the examples so far, with the leading Π\Pi over the parameters omitted from the constructor types:

Type former Parameters Indices Constructors
Nat:Sort 1\Nat : \Sort{1} none none zero:Nat\zero : \Nat,   succ:Nat→Nat\;\suc : \Nat \to \Nat
Tree:Sort 1\mathsf{Tree} : \Sort{1} none none leaf\mathsf{leaf}, node\mathsf{node}, branch\mathsf{branch}
List:Sort 1→Sort 1\mathsf{List} : \Sort{1} \to \Sort{1} (A:Sort 1)(A : \Sort{1}) none nil:List  A\mathsf{nil} : \mathsf{List}\;A,   cons:A→List  A→List  A\;\mathsf{cons} : A \to \mathsf{List}\;A \to \mathsf{List}\;A
Vec:Sort 1→Nat→Sort 1\mathsf{Vec} : \Sort{1} \to \Nat \to \Sort{1} (A:Sort 1)(A : \Sort{1}) (n:Nat)(n : \Nat) vnil:Vec  A  zero\mathsf{vnil} : \mathsf{Vec}\;A\;\zero,   vcons:Π(n:Nat). A→Vec  A  n→Vec  A  (succ  n)\;\mathsf{vcons} : \tPi{n}{\Nat}{A \to \mathsf{Vec}\;A\;n \to \mathsf{Vec}\;A\;(\suc\;n)}

Acceptance. The kernel accepts a declaration only if the following four conditions hold.

  1. The type of II is a type in the current signature.
  2. With II added to the signature, each constructor type is a type, and it ends in I  p⃗  t⃗jI\;\vec{p}\;\vec{t}_j, where p⃗\vec{p} are exactly the parameters in order and II does not occur in t⃗j\vec{t}_j.
  3. Strict positivity. Each field type either does not mention II, or has the form Π(y⃗:B⃗). I  p⃗  s⃗\tPi{\vec{y}}{\vec{B}}{I\;\vec{p}\;\vec{s}} where II occurs neither in B⃗\vec{B} nor in s⃗\vec{s}. A field of the second form is recursive.
  4. Universes. If u≥1u \ge 1, each field type lies in some Sort w\Sort{w} with w≤uw \le u. If u=0u = 0, there is no condition.

Condition 2 lets the recursor treat the parameters uniformly. Condition 3 rules out declarations such as

inductive Bad where
  | mk : (Bad -> False) -> Bad

whose field mentions Bad to the left of an arrow. Case analysis on Bad would let one define f:Bad→Falsef : \mathsf{Bad} \to \mathsf{False} by f  (mk  g)=g  (mk  g)f\;(\mathsf{mk}\;g) = g\;(\mathsf{mk}\;g). Then f  (mk  f)f\;(\mathsf{mk}\;f) is a closed term of type False\mathsf{False}, and it reduces to itself, so it has no normal form. For a type Tree\mathsf{Tree} under declaration, some field types and their status are:

Field type Status Reason
Nat\Nat accepted, not recursive does not mention Tree\mathsf{Tree}
Tree\mathsf{Tree} accepted, recursive induction hypothesis M  akM\;a_k
Nat→Tree\Nat \to \mathsf{Tree} accepted, recursive produces trees; hypothesis Π(n:Nat). M  (ak  n)\tPi{n}{\Nat}{M\;(a_k\;n)}
Tree→Nat\mathsf{Tree} \to \Nat rejected Tree\mathsf{Tree} to the left of an arrow
List  Tree\mathsf{List}\;\mathsf{Tree} rejected here nested occurrence; Lean accepts it, but it is out of scope

A field of type Nat→Tree\Nat \to \mathsf{Tree} is a tree with infinitely many children, one for each number; the recursor can still recurse into each child ak  na_k\;n. A field of type Tree→Nat\mathsf{Tree} \to \Nat consumes trees instead of containing them, and it is the shape that makes Bad paradoxical.

Condition 4 prevents a type from containing a larger universe. Without it, a declaration T:Sort 1T : \Sort{1} with a constructor mk:Sort 1→T\mathsf{mk} : \Sort{1} \to T would place a copy of all of Sort 1\Sort{1} inside Sort 1\Sort{1}, which is the self-containment that Chapter 13 shows to be inconsistent. For u=0u = 0 no condition is needed, because Sort 0\Sort{0} is impredicative.

The recursor. For an accepted declaration, the kernel generates a recursor I.recvI.\mathsf{rec}_v for each admissible level vv (see below):

I.recv:Π(p⃗:P⃗).   Π(M:(Π(ı⃗:J⃗). I  p⃗  ı⃗→Sort v)).   Π(m1:C1). ⋯ Π(mn:Cn).   Π(ı⃗:J⃗). Π(x:I  p⃗  ı⃗). M  ı⃗  x.I.\mathsf{rec}_v : \tPi{\vec{p}}{\vec{P}}{\; \tPi{M}{(\tPi{\vec{\imath}}{\vec{J}}{I\;\vec{p}\;\vec{\imath} \to \Sort{v}})}{\; \tPi{m_1}{C_1}{\cdots\,\tPi{m_n}{C_n}{\; \tPi{\vec{\imath}}{\vec{J}}{\tPi{x}{I\;\vec{p}\;\vec{\imath}}{M\;\vec{\imath}\;x}}}}}}.

The parameters come first and stay fixed, as for lists; the motive takes the indices and the element, as for vectors. The minor premise CjC_j asks for the case of constructor cjc_j: given its fields and an induction hypothesis for each recursive field, build MM for cjc_j applied to those fields:

Cj=Π(a⃗:A⃗j).   Π(h⃗:H⃗j).   M  t⃗j  (cj  p⃗  a⃗),Hj,k=Π(y⃗:B⃗). M  s⃗  (ak  y⃗)for each recursive field ak:Π(y⃗:B⃗). I  p⃗  s⃗.\begin{array}{l} C_j = \tPi{\vec{a}}{\vec{A}_j}{\;\tPi{\vec{h}}{\vec{H}_j}{\;M\;\vec{t}_j\;(c_j\;\vec{p}\;\vec{a})}}, \\[4pt] H_{j,k} = \tPi{\vec{y}}{\vec{B}}{M\;\vec{s}\;(a_k\;\vec{y})} \quad\text{for each recursive field } a_k : \tPi{\vec{y}}{\vec{B}}{I\;\vec{p}\;\vec{s}}. \end{array}

The final arguments are the indices and the element xx itself, the major premise.

The rules. For each constructor, the kernel generates one iota-rule, which applies the minor premise to the fields and to the induction hypotheses, computed by recursive calls:

I.recv  p⃗  M  m⃗  ı⃗  (cj  p⃗ ′  a⃗)  →ι  mj  a⃗  h⃗,hk=λ(y⃗:B⃗). I.recv  p⃗  M  m⃗  s⃗  (ak  y⃗).I.\mathsf{rec}_v\;\vec{p}\;M\;\vec{m}\;\vec{\imath}\;(c_j\;\vec{p}\,'\;\vec{a}) \;\iotared\; m_j\;\vec{a}\;\vec{h}, \qquad h_k = \tLam{\vec{y}}{\vec{B}}{I.\mathsf{rec}_v\;\vec{p}\;M\;\vec{m}\;\vec{s}\;(a_k\;\vec{y})}.

The major premise is at position ∣p⃗∣+1+n+∣ı⃗∣|\vec{p}| + 1 + n + |\vec{\imath}| (counting from zero), the constructor has ∣p⃗∣|\vec{p}| parameters and ∣a⃗∣|\vec{a}| fields, and in a well-typed redex p⃗ ′\vec{p}\,' is convertible to p⃗\vec{p}, so the rule ignores it, as the rules for lists ignore A′A'. For Nat\Nat, Tree\mathsf{Tree}, List\mathsf{List}, and Vec\mathsf{Vec} the procedure produces exactly the recursors and rules of this chapter and the last, and the recursor types agree with the ones Lean 4 generates, up to the names of bound variables and implicit arguments.

Eliminating from propositions. If u≥1u \ge 1, the recursor is generated for every level vv. If u=0u = 0, so that II is a proposition, the recursor is generated only for v=0v = 0, unless II is a subsingleton: it has at most one constructor, and each field either is a proof (its type lies in Sort 0\Sort{0}) or occurs among the index terms t⃗j\vec{t}_j. The restriction keeps data from depending on which proof of a proposition was given. For example, P∨QP \vee Q has two constructors, so a function from proofs of P∨QP \vee Q into Nat\Nat could return 00 for one and 11 for the other, and a proof would carry computational information. Lean's Or.rec and Exists.rec therefore eliminate only into Prop, while And.rec and Eq.rec eliminate into every universe: And has one constructor whose fields are proofs, and Eq (Chapter 9) has one constructor with no fields.

So the kernel's rules stay fixed while the signature grows: one generic procedure turns each declaration into signature entries, and a single reduction function runs their rules (Chapter 12).

In the code

The heart of the generator is short enough to read in full. Compare it with the formula for CjC_j.

/// C_j = Pi (as : As_j). Pi (hs : Hs_j). M ts_j (c_j ps as)
fn minor_premise_type(info: &InductiveInfo, j: usize, constructor: &ConstructorInfo) -> Term {
    // c_j ps as
    let constructor_application = apps(
        const_term(&constructor.name),
        info.params.iter().chain(constructor.fields.iter()).map(|(name, _)| var(name)),
    );
    // (as : As_j) followed by (hs : Hs_j)
    let mut minor_premise_binders = constructor.fields.clone();
    for (k, recursive_field) in constructor.recursive_fields.iter().enumerate() {
        minor_premise_binders.push((
            format!("{INDUCTION_HYPOTHESIS_PREFIX}{j}_{k}"),
            induction_hypothesis_type(constructor, recursive_field),
        ));
    }
    telescope(&minor_premise_binders, apply_motive(&constructor.result_indices, constructor_application))
}

The kernel never special-cases Nat\Nat. Kernel::standard in src/kernel.rs declares Nat\Nat and Eq\mathsf{Eq} by calling add_inductive with the same InductiveSpec a user script would produce, so Figure 7.1 is generated, not hard-coded, and the tests in src/inductive/tests.rs check that the generated types agree with the hand-written ones.

Exercises

Write the recursor type Bool.recv\mathsf{Bool.rec}_v for inductive Bool where | true | false, and define negation with it.

Solution

Bool.recv:Π(M:Bool→Sort v). M  true→M  false→Π(b:Bool). M  b\mathsf{Bool.rec}_v : \tPi{M}{\mathsf{Bool} \to \Sort{v}}{M\;\mathsf{true} \to M\;\mathsf{false} \to \tPi{b}{\mathsf{Bool}}{M\;b}}, with no induction hypotheses since neither constructor has fields. Negation is λ(b:Bool). Bool.rec1  (λ(_:Bool). Bool)  false  true  b\tLam{b}{\mathsf{Bool}}{\mathsf{Bool.rec}_1\;(\tLam{\_}{\mathsf{Bool}}{\mathsf{Bool}})\;\mathsf{false}\;\mathsf{true}\;b}. The recursor of a type without recursive fields is just case analysis.

Which of the four acceptance conditions rejects each of the following, if any? (a) inductive T : Sort 1 where | mk (f : Sort 1). (b) inductive T : Sort 1 where | mk (f : T -> T). (c) inductive T : Sort 0 where | mk (f : Sort 1).

Solution

(a) Condition 4: the field Sort 1\Sort{1} has type Sort 2\Sort{2}, which exceeds u=1u = 1. (b) Condition 3: TT occurs to the left of an arrow in a field type. (c) Accepted: for u=0u = 0 condition 4 imposes nothing, and the type is a proposition that packs up a type; this is the impredicativity of Sort 0\Sort{0} at work, and it is harmless because TT eliminates only into Sort 0\Sort{0} (it is not a subsingleton, since its field is neither a proof nor an index).

What to remember

  • A declaration gives a type former with parameters and indices, and constructors with fields. Parameters are fixed throughout; indices vary and become arguments of the motive.
  • The recursor has one minor premise per constructor, with one induction hypothesis per recursive field, and one iota-rule per constructor.
  • Acceptance requires well-formedness, uniform parameters, strict positivity, and a universe bound. Positivity is what keeps recursion terminating and the logic consistent.
  • Propositions eliminate only into Sort 0\Sort{0} unless they are subsingletons.
  • The kernel generates all of this from data; it contains no code specific to Nat\Nat.