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
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, is a tree built from four constructors. To build for every tree , where , the recipe of Chapter 7 asks for one case per constructor, and each argument of type contributes one induction hypothesis:
The recursor takes the motive, the three cases, and a tree:
where the dots stand for the types of and . Compared with , only the number of cases and the number of hypotheses per case have changed. Writing for the partial application , the iota-rules are
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 and the cases
counts the constructors of a tree:
With a predicate (), the same recursor is structural induction on trees. In general, a type with constructors gets a recursor with minor premises , 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 and 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 with constructors and . But then lists of trees would need another declaration, , 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 and are two instances of one declaration. The declaration adds
The constructors take as an explicit first argument. For example, the one-element list is . Within one list, never changes: the tail of is again 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:
Apart from the leading , this is the recipe of Chapter 7 once more: has two arguments, of which only is recursive, so its case gets one induction hypothesis. The rules ignore the parameter stored in the constructor; in a well-typed term it is convertible to . For example, the length of a list is
A list value carries the parameter 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 may have any length. What if we want the length to be part of the type, so that and 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 is still a parameter. The length is not: has length , and takes a vector of length and returns one of length . 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
Because an index varies, the recursor cannot fix it in advance. Instead, the motive takes it as an argument, , and the case for is
The induction hypothesis is about a vector of length , read off the type of the recursive argument, and the conclusion is about one of length , 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 just as readily as , 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 for a sequence of bindings in which each may mention ; for ; and for . The length of is . In the code, a telescope is a Context, and telescope(binders, tail) in src/term.rs builds the nested .
Declarations. A declaration introduces a type former and constructors :
The parameters are shared by all constructors and appear unchanged in every result type. The indices may differ between constructors: the index terms may mention and the constructor's own arguments , which are called its fields. For the examples so far, with the leading over the parameters omitted from the constructor types:
| Type former | Parameters | Indices | Constructors |
|---|---|---|---|
| none | none | , | |
| none | none | , , | |
| none | , | ||
| , |
Acceptance. The kernel accepts a declaration only if the following four conditions hold.
- The type of is a type in the current signature.
- With added to the signature, each constructor type is a type, and it ends in , where are exactly the parameters in order and does not occur in .
- Strict positivity. Each field type either does not mention , or has the form where occurs neither in nor in . A field of the second form is recursive.
- Universes. If , each field type lies in some with . If , 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 by . Then is a closed term of type , and it reduces to itself, so it has no normal form. For a type under declaration, some field types and their status are:
| Field type | Status | Reason |
|---|---|---|
| accepted, not recursive | does not mention | |
| accepted, recursive | induction hypothesis | |
| accepted, recursive | produces trees; hypothesis | |
| rejected | to the left of an arrow | |
| rejected here | nested occurrence; Lean accepts it, but it is out of scope |
A field of type is a tree with infinitely many children, one for each number; the recursor can still recurse into each child . A field of type 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 with a constructor would place a copy of all of inside , which is the self-containment that Chapter 13 shows to be inconsistent. For no condition is needed, because is impredicative.
The recursor. For an accepted declaration, the kernel generates a recursor for each admissible level (see below):
The parameters come first and stay fixed, as for lists; the motive takes the indices and the element, as for vectors. The minor premise asks for the case of constructor : given its fields and an induction hypothesis for each recursive field, build for applied to those fields:
The final arguments are the indices and the element 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:
The major premise is at position (counting from zero), the constructor has parameters and fields, and in a well-typed redex is convertible to , so the rule ignores it, as the rules for lists ignore . For , , , and 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 , the recursor is generated for every level . If , so that is a proposition, the recursor is generated only for , unless is a subsingleton: it has at most one constructor, and each field either is a proof (its type lies in ) or occurs among the index terms . The restriction keeps data from depending on which proof of a proposition was given. For example, has two constructors, so a function from proofs of into could return for one and 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 .
/// 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 . Kernel::standard in src/kernel.rs declares and 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 for inductive Bool where | true | false, and define negation with it.
Solution
, with no induction hypotheses since neither constructor has fields. Negation is . 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 has type , which exceeds . (b) Condition 3: occurs to the left of an arrow in a field type. (c) Accepted: for condition 4 imposes nothing, and the type is a proposition that packs up a type; this is the impredicativity of at work, and it is harmless because eliminates only into (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 unless they are subsingletons.
- The kernel generates all of this from data; it contains no code specific to .