Lean's Inductive Types
Inductive types in Lean allow for conservative extensions of the core theory (the calculus of inductive constructions) by adding new freely generated types. Here “conservative” is a term of art: it means that our additions do not “increase the power” of the underlying theory by adding new axioms; instead, they extend the language in a way designed to preserve consistency.
But how does this work internally?
What exactly is added to the logical theory when we write inductive ...?
This post answers that question using natural numbers as the running example.
Suppose we declare a Nat type as follows:
namespace MyNat
inductive Nat : Type
| zero : Nat
| succ : Nat → Nat
end MyNat
When we do this, Lean adds constructors, or “introduction rules”:
Nat.zero : Nat
Nat.succ : Nat → Nat
These let us construct (or ‘introduce’) Nat values.
For example, the value for 1 is introduced as Nat.succ Nat.zero : Nat.
But we also need to deconstruct or (‘eliminate’) a Nat value.
Lean adds a “recursor” Nat.rec for this, whose type can be printed using
#check Nat.rec:
MyNat.Nat.rec.{u}
{motive : Nat → Sort u}
(zero : motive Nat.zero)
(succ : (a : Nat) → motive a → motive a.succ)
(t : Nat) : motive t
Spelling this out, we have:
- A dependent function
motiveassigning to eachNatvaluensome type denotedmotive n. - A base case - a value
zeroof typemotive Nat.zero - An inductive case - a function
succmapping a value of typemotive ato a valuemotive (Nat.succ a)
With these three arguments applied, we’re left with a function of type
(t : Nat) → motive t.
This is a dependent function type mapping each nat value to a result whose
type depends on the value.
A motivating proof-shaped use is ∀ n, n = n.
In Lean, proving this means constructing a term of that type.
Nat.rec is exactly the induction/recursion principle that lets us build such
terms by giving:
1. a value at zero, and
2. a way to extend a value at a to one at succ a.
Together, the constructors (introduction rules) and recursor (elimination rule) form the core logical/computational content of an inductive declaration. However, Lean also automatically derives a couple useful utilities.
Helpers: casesOn and noConfusion
In addition to rec, lean adds a special case: Nat.casesOn, which lets us do
a ‘shallow match’ of cases, without recursing.
-- casesOn
#check Nat.casesOn
-- MyNat.Nat.casesOn.{u} {motive : Nat → Sort u} (t : Nat) (zero : motive Nat.zero) (succ : (a : Nat) → motive a.succ) :
motive t
As a quick aside, note that if you try to add a case to Nat with the same name as one of these automatically added functions, you will get an error! E.g., adding
inductive Nat : Type
| zero : Nat
| casesOn : Nat -- will cause an error
| succ : Nat → Nat
… will cause an error like this:
error: (kernel) constant has already been declared 'MyNat.Nat.casesOn'
A more important helper is Nat.noConfusion.
This is a theorem that says if t = t', then:
- Matching constructors imply equal arguments
- Different constructors are impossible
In table form:
t |
t' |
??? |
|---|---|---|
zero |
zero |
Trivial - no args to compare |
zero |
succ a |
Impossible - different constructors |
succ a |
zero |
Impossible - different constructors |
succ a |
succ a₁ |
a = a₁ |
Note that noConfusion is purely a helper: we could write it by hand, but it’s both tedious
and mechanically derivable, so Lean gives it to us automatically.
How does this work? Let’s examine Nat.noConfusion by #checking it first.
-- #check Nat.noConfusion
MyNat.Nat.noConfusion.{u} {P : Sort u} {t t' : Nat} (eq : t = t') : Nat.noConfusionType P t t'
This isn’t particularly helpful until we examine the definition of noConfusionType.
Informally, it unpacks as the following case analysis:
t |
t' |
Nat.noConfusionType P t t' |
|---|---|---|
zero |
zero |
P → P |
zero |
succ a |
P |
succ a |
zero |
P |
succ a |
succ a₁ |
(a = a₁ → P) → P |
More precisely, when we #print Nat.noConfusionType, we get this nested case
analysis which first unpacks t, then t' within each branch.
I’ve indented the #print for easier reading:
@[reducible] protected def MyNat.Nat.noConfusionType.{u} : Sort u → Nat → Nat → Sort u :=
fun P t t' =>
Nat.casesOn t
(Nat.casesOn t'
(P → P)
fun a => P
)
(fun a => Nat.casesOn t'
P
(fun a_1 => (a = a_1 → P) → P)
)
Finally, Lean also automatically adds an injectivity helper
Nat.succ.inj as a useful special case proof that Succ(a) = Succ(a') ⇒ a = a'.