We have seen that Lean's formal foundation includes basic types,
Prop, Type0, Type1, Type2, ..., and allows for the formation of
dependent function types, (x:α)→β. In the examples, we have
also made use of additional types like Bool, Nat, and Int,
and type constructors, like List, and product, ×. In fact, in
Lean's library, every concrete type other than the universes and every
type constructor other than dependent arrows is an instance of a general family of
type constructions known as inductive types. It is remarkable that
it is possible to construct a substantial edifice of mathematics based
on nothing more than the type universes, dependent arrow types, and inductive
types; everything else follows from those.
Intuitively, an inductive type is built up from a specified list of
constructors. In Lean, the syntax for specifying such a type is as
follows:
The intuition is that each constructor specifies a way of building new
objects of Foo, possibly from previously constructed values. The
type Foo consists of nothing more than the objects that are
constructed in this way.
We will see below that the arguments of the constructors can include
objects of type Foo, subject to a certain “positivity” constraint,
which guarantees that elements of Foo are built from the bottom
up. Roughly speaking, each ... can be any arrow type constructed from
Foo and previously defined types, in which Foo appears, if at
all, only as the “target” of the dependent arrow type.
We will provide a number of examples of inductive types. We will also
consider slight generalizations of the scheme above, to mutually
defined inductive types, and so-called inductive families.
As with the logical connectives, every inductive type comes with
introduction rules, which show how to construct an element of the
type, and elimination rules, which show how to “use” an element of the
type in another construction. The analogy to the logical connectives
should not come as a surprise; as we will see below, they, too, are
examples of inductive type constructions. You have already seen the
introduction rules for an inductive type: they are just the
constructors that are specified in the definition of the type. The
elimination rules provide for a principle of recursion on the type,
which includes, as a special case, a principle of induction as well.
In the next chapter, we will describe Lean's function definition
package, which provides even more convenient ways to define functions
on inductive types and carry out inductive proofs. But because the
notion of an inductive type is so fundamental, we feel it is important
to start with a low-level, hands-on understanding. We will start with
some basic examples of inductive types, and work our way up to more
elaborate and complex examples.
Think of sunday, monday, ... , saturday as
being distinct elements of Weekday, with no other distinguishing
properties. The elimination principle, Weekday.rec, is defined
along with the type Weekday and its constructors. It is also known
as a recursor, and it is what makes the type “inductive”: it allows
us to define a function on Weekday by assigning values
corresponding to each constructor. The intuition is that an inductive
type is exhaustively generated by the constructors, and has no
elements beyond those they construct.
When using Lean's logic, the match expression is compiled using the recursorWeekday.rec generated when
you declare the inductive type. This ensures that the resulting term is well-defined in the type theory. For compiled code,
match is compiled as in other functional programming languages.
When declaring an inductive datatype, you can use derivingRepr to instruct
Lean to generate a function that converts Weekday objects into text.
This function is used by the #eval command to display Weekday objects.
If no Repr exists, #eval attempts to derive one on the spot.
It is often useful to group definitions and theorems related to a
structure in a namespace with the same name. For example, we can put
the numberOfDay function in the Weekday namespace. We are
then allowed to use the shorter name when we open the namespace.
Tactics for Inductive Types below will introduce additional
tactics that are specifically designed to make use of inductive types.
Notice that, under the propositions-as-types correspondence, we can
use match to prove theorems as well as define functions. In other
words, under the propositions-as-types correspondence, the proof by
cases is a kind of definition by cases, where what is being “defined”
is a proof instead of a piece of data.
The Bool type in the Lean library is an instance of
enumerated type.
(To run these examples, we put them in a namespace called Hidden,
so that a name like Bool does not conflict with the Bool in
the standard library. This is necessary because these types are part
of the Lean “prelude” that is automatically imported when the system
is started.)
As an exercise, you should think about what the introduction and
elimination rules for these types do. As a further exercise, we
suggest defining boolean operations and, or, not on the
Bool type, and verifying common identities. Note that you can define a
binary operation like and using match:
Enumerated types are a very special case of inductive types, in which
the constructors take no arguments at all. In general, a
“construction” can depend on data, which is then represented in the
constructed argument. Consider the definitions of the product type and
sum type in the library:
Consider what is going on in these examples.
The product type has one constructor, Prod.mk,
which takes two arguments. To define a function on Prodαβ, we
can assume the input is of the form Prod.mkab, and we have to
specify the output, in terms of a and b. We can use this to
define the two projections for Prod. Remember that the standard
library defines notation α×β for Prodαβ and (a,b) for
Prod.mkab.
deffst{α:Typeu}{β:Typev}(p:Prodαβ):α:=matchpwith|Prod.mkaVariable name `b` is not explicitly referenced.Hint: The binding can be removed (if unused) or named `_` (if used implicitly). Alternatively, prefix the name with `_` to silence this warning:[apply]_bNote: This linter can be disabled with `set_option linter.unusedVariables false`b=>adefsnd{α:Typeu}{β:Typev}(p:Prodαβ):β:=matchpwith|Prod.mkVariable name `a` is not explicitly referenced.Hint: The binding can be removed (if unused) or named `_` (if used implicitly). Alternatively, prefix the name with `_` to silence this warning:[apply]_aNote: This linter can be disabled with `set_option linter.unusedVariables false`ab=>b
endHidden
The function fst takes a pair, p. The match interprets
p as a pair, Prod.mkab. Recall also from Dependent Type Theory
that to give these definitions the greatest generality possible, we allow
the types α and β to belong to any universe.
Here is another example where we use the recursor Prod.casesOn instead
of match.
The argument motive is used to specify the type of the object you want to
construct, and it is a function because it may depend on the pair.
The cond function is a boolean conditional: condbt1t2
returns t1 if b is true, and t2 otherwise.
The function prod_example takes a pair consisting of a boolean,
b, and a number, n, and returns either 2*n or 2*n+1
according to whether b is true or false.
In contrast, the sum type has two constructors, inl and inr
(for “insert left” and “insert right”), each of which takes one
(explicit) argument. To define a function on Sumαβ, we have to
handle two cases: either the input is of the form inla, in which
case we have to specify an output value in terms of a, or the
input is of the form inrb, in which case we have to specify an
output value in terms of b.
This example is similar to the previous one, but now an input to
sum_example is implicitly either of the form inln or inrn.
In the first case, the function returns 2*n, and the second
case, it returns 2*n+1.
Notice that the product type depends on parameters αβ : Type
which are arguments to the constructors as well as Prod. Lean
detects when these arguments can be inferred from later arguments to a
constructor or the return type, and makes them implicit in that case.
In Defining the Natural Numbers
we will see what happens when the
constructor of an inductive type takes arguments from the inductive
type itself. What characterizes the examples we consider in this
section is that each constructor relies only on previously specified types.
Notice that a type with multiple constructors is disjunctive: an
element of Sumαβ is either of the form inlaor of the
form inlb. A constructor with multiple arguments introduces
conjunctive information: from an element Prod.mkab of
Prodαβ we can extract aandb. An arbitrary inductive type can
include both features, by having any number of constructors, each of
which takes any number of arguments.
As with function definitions, Lean's inductive definition syntax will
let you put named arguments to the constructors before the colon:
The results of these definitions are essentially the same as the ones given earlier in this section.
A type, like Prod, that has only one constructor is purely
conjunctive: the constructor simply packs the list of arguments into a
single piece of data, essentially a tuple where the type of subsequent
arguments can depend on the type of the initial argument. We can also
think of such a type as a “record” or a “structure”. In Lean, the
keyword structure can be used to define such an inductive type as
well as its projections, at the same time.
This example simultaneously introduces the inductive type, Prod,
its constructor, mk, the usual eliminators (rec and
recOn), as well as the projections, fst and snd, as
defined above.
If you do not name the constructor, Lean uses mk as a default. For
example, the following defines a record to store a color as a triple
of RGB values:
The definition of yellow forms the record with the three values
shown, and the projection Color.red returns the red component.
The structure command is especially useful for defining algebraic
structures, and Lean provides substantial infrastructure to support
working with them. Here, for example, is the definition of a
semigroup:
In the semantics of dependent type theory, there is no built-in notion
of a partial function. Every element of a function type α→β or a
dependent function type (a:α)→β is assumed to have a value
at every input. The Option type provides a way of representing partial functions. An
element of Optionβ is either none or of the form someb,
for some value b : β. Thus we can think of an element f of the
type α→Optionβ as being a partial function from α to β:
for every a : α, fa either returns none, indicating
fa is “undefined”, or someb.
An element of Inhabitedα is simply a witness to the fact that
there is an element of α. Later, we will see that Inhabited is
an example of a type class in Lean: Lean can be instructed that
suitable base types are inhabited, and can automatically infer that
other constructed types are inhabited on that basis.
As exercises, we encourage you to develop a notion of composition for
partial functions from α to β and β to γ, and show
that it behaves as expected. We also encourage you to show that
Bool and Nat are inhabited, that the product of two inhabited
types is inhabited, and that the type of functions to an inhabited
type is inhabited.
Inductively defined types can live in any type universe, including the
bottom-most one, Prop. In fact, this is exactly how the logical
connectives are defined.
You should think about how these give rise to the introduction and
elimination rules that you have already seen. There are rules that
govern what the eliminator of an inductive type can eliminate to,
that is, what kinds of types can be the target of a recursor. Roughly
speaking, what characterizes inductive types in Prop is that one
can only eliminate to other types in Prop. This is consistent with
the understanding that if p : Prop, an element hp : p carries
no data. There is a small exception to this rule, however, which we
will discuss below, in Inductive Families.
Even the existential quantifier is inductively defined:
Keep in mind that the notation ∃x:α,p is syntactic sugar for Exists(funx:α=>p).
The definitions of False, True, And, and Or are
perfectly analogous to the definitions of Empty, Unit,
Prod, and Sum. The difference is that the first group yields
elements of Prop, and the second yields elements of Typeu for
some u. In a similar way, ∃x:α,p is a Prop-valued
variant of Σx:α,β.
This is a good place to mention another inductive type, denoted
{x:α//p}, which is sort of a hybrid between
∃x:α,p and Σx:α,β.
The notation {x:α//px} is syntactic sugar for Subtype(funx:α=>px).
It is modeled after subset notation in set theory: the idea is that {x:α//px}
denotes the collection of elements of α that have property p.
The inductively defined types we have seen so far are “flat”:
constructors wrap data and insert it into a type, and the
corresponding recursor unpacks the data and acts on it. Things get
much more interesting when the constructors act on elements of the
very type being defined. A canonical example is the type Nat of
natural numbers:
There are two constructors. We start with zero : Nat; it takes
no arguments, so we have it from the start. In contrast, the
constructor succ can only be applied to a previously constructed
Nat. Applying it to zero yields succzero:Nat. Applying
it again yields succ(succzero):Nat, and so on. Intuitively,
Nat is the “smallest” type with these constructors, meaning that
it is exhaustively (and freely) generated by starting with zero
and applying succ repeatedly.
As before, the recursor for Nat is designed to define a dependent
function f from Nat to any domain, that is, an element f
of (n:Nat)→motiven for some motive : Nat→Sortu.
It has to handle two cases: the case where the input is zero, and the case where
the input is of the form succn for some n : Nat. In the first
case, we simply specify a target value with the appropriate type, as
before. In the second case, however, the recursor can assume that a
value of f at n has already been computed. As a result, the
next argument to the recursor specifies a value for f(succn) in
terms of n and fn. If we check the type of the recursor,
you find the following:
The implicit argument, motive, is the codomain of the function being defined.
In type theory it is common to say motive is the motive for the elimination/recursion,
since it describes the kind of object we wish to construct.
The next two arguments specify how to compute the zero and successor cases, as described above.
They are also known as the minor premises.
Finally, the t:Nat is the input to the function. It is also known as the major premise.
The Nat.recOn is similar to Nat.rec but the major premise occurs before the minor premises.
Consider, for example, the addition function addmn on the
natural numbers. Fixing m, we can define addition by recursion on
n. In the base case, we set addmzero to m. In the
successor step, assuming the value addmn is already determined,
we define addm(succn) to be succ(addmn).
inductiveNatwhere|zero:Nat|succ:Nat→NatderivingReprdefadd(mn:Nat):Nat:=matchnwith|Nat.zero=>m|Nat.succn=>Nat.succ(addmn)openAmbiguous namespace `Nat`: it is interpreted as `_root_.Hidden.Nat` because this `open` occurs inside `namespace Hidden`, while `_root_.Nat` is silently not opened. Specify the namespace unambiguously, e.g. `_root_.Hidden.Nat`. The warning can sometimes also be addressed by moving the `open` outside of the surrounding `namespace`.Note: This linter can be disabled with `set_option linter.ambiguousOpen false`NatHidden.Nat.succ (Hidden.Nat.succ (Hidden.Nat.succ (Hidden.Nat.zero)))#evaladd(succ(succzero))(succzero)
It is useful to put such definitions into a namespace, Nat. We can
then go on to define familiar notation in that namespace. The two
defining equations for addition now hold definitionally:
We will explain how the instance command works in
the Type Classes chapter. In the examples below, we will use
Lean's version of the natural numbers.
Proving a fact like 0+n=n, however, requires a proof by induction.
As observed above, the induction principle is just a special case of the recursion principle,
when the codomain motiven is an element of Prop. It represents the familiar
pattern of an inductive proof: to prove ∀n,motiven, first prove motive0,
and then, for arbitrary n, assume ih : motiven and prove motive(n+1).
Notice that, once again, when Nat.recOn is used in the context of
a proof, it is really the induction principle in disguise. The
rw and simp tactics tend to be very effective in proofs
like these. In this case, each can be used to reduce the proof to:
openNattheoremzero_add(n:Nat):0+n=n:=Nat.recOn(motive:=funx=>0+x=x)nrfl(funnih=>byn✝:Natn:Natih:0+n=n⊢ 0+n.succ=n.succsimp[This simp argument is unused:ihHint: Omit it from the simp argument list.[apply]simpNote: This linter can be disabled with `set_option linter.unusedSimpArgs false`ih]All goals completed! 🐙)
endHidden
As another example, let us prove the associativity of addition,
∀mnk,m+n+k=m+(n+k).
(The notation +, as we have defined it, associates to the left, so m+n+k is really (m+n)+k.)
The hardest part is figuring out which variable to do the induction on. Since addition is defined by recursion on the second argument,
k is a good guess, and once we make that choice the proof almost writes itself:
openAmbiguous namespace `Nat`: it is interpreted as `_root_.Hidden.Nat` because this `open` occurs inside `namespace Hidden`, while `_root_.Nat` is silently not opened. Specify the namespace unambiguously, e.g. `_root_.Hidden.Nat`. The warning can sometimes also be addressed by moving the `open` outside of the surrounding `namespace`.Note: This linter can be disabled with `set_option linter.ambiguousOpen false`Nattheoremsucc_add(nm:Nat):succn+m=succ(n+m):=Nat.recOn(motive:=funx=>succn+x=succ(n+x))mrfl(funmih=>byn:Natm✝:Natm:Natih:n.succ+m=(n+m).succ⊢ n.succ+m.succ=(n+m.succ).succsimpa[add_succ(succn)]All goals completed! 🐙)theoremadd_comm(mn:Nat):m+n=n+m:=Nat.recOn(motive:=funx=>m+x=x+m)n(bym:Natn:Nat⊢ m+zero=zero+msimp[add_zero,zero_add]All goals completed! 🐙)(funmih=>bym✝:Natn:Natm:Natih:m✝+m=m+m✝⊢ m✝+m.succ=m.succ+m✝simp_all[succ_add,add_succ]All goals completed! 🐙)
A list of elements of type α is either the empty list, nil, or
an element h:α followed by a list t:Listα.
The first element, h, is commonly known as the “head” of the list,
and the remainder, t, is known as the “tail.”
Try also defining the function length : {α:Typeu}→Listα→Nat that returns the length of a list,
and prove that it behaves as expected (for example, length(appendasbs)=lengthas+lengthbs).
For another example, we can define the type of binary trees:
Given the fundamental importance of inductive types in Lean, it should
not be surprising that there are a number of tactics designed to work
with them effectively. We describe some of them here.
The cases tactic works on elements of an inductively defined type,
and does what the name suggests: it decomposes the element according
to each of the possible constructors. In its most basic form, it is
applied to an element x in the local context. It then reduces the
goal to cases in which x is replaced by each of the constructions.
There are extra bells and whistles. For one thing, cases allows
you to choose the names for each alternative using a
with clause. In the next example, for example, we choose the name
m for the argument to succ, so that the second case refers to
succm. More importantly, the cases tactic will detect any items
in the local context that depend on the target variable. It reverts
these elements, does the split, and reintroduces them. In the example
below, notice that the hypothesis h:n≠0 becomes h:0≠0
in the first branch, and h:m+1≠0 in the second.
The syntax of the with is convenient for writing structured proofs.
Lean also provides a complementary case tactic, which allows you to focus on goal
assign variable names.
The case tactic is clever, in that it will match the constructor to the appropriate goal. For example, we can fill the goals above in the opposite order:
You can also use cases with an arbitrary expression. Assuming that
expression occurs in the goal, the cases tactic will generalize over
the expression, introduce the resulting universally quantified
variable, and case on that.
Think of this as saying “split on cases as to whether m+3*k is
zero or the successor of some number.” The result is functionally
equivalent to the following:
Notice that the expression m+3*k is erased by generalize; all
that matters is whether it is of the form 0 or n✝+1. This
form of cases will not revert any hypotheses that also mention
the expression in the equation (in this case, m+3*k). If such a
term appears in a hypothesis and you want to generalize over that as
well, you need to revert it explicitly.
If the expression you case on does not appear in the goal, the
cases tactic uses have to put the type of the expression into
the context. Here is an example:
The theorem Nat.lt_or_gemn says m<n ∨ m≥n, and it is
natural to think of the proof above as splitting on these two
cases. In the first branch, we have the hypothesis hlt:m<n, and
in the second we have the hypothesis hge:m≥n. The proof above
is functionally equivalent to the following:
Remember that if you open Classical, you can use the law of the
excluded middle for any proposition at all. But using type class
inference (see Type Classes), Lean can actually
find the relevant decision procedure, which means that you can use the
case split in a computable function.
Just as the cases tactic can be used to carry out proof by cases,
the induction tactic can be used to carry out proofs by
induction. The syntax is similar to that of cases, except that the
argument can only be a term in the local context. Here is an example:
openAmbiguous namespace `Nat`: it is interpreted as `_root_.Hidden.Nat` because this `open` occurs inside `namespace Hidden`, while `_root_.Nat` is silently not opened. Specify the namespace unambiguously, e.g. `_root_.Hidden.Nat`. The warning can sometimes also be addressed by moving the `open` outside of the surrounding `namespace`.Note: This linter can be disabled with `set_option linter.ambiguousOpen false`Nattheoremzero_add(n:Nat):0+n=n:=byn:Nat⊢ 0+n=ninductionnzero⊢ 0+zero=zerosucca✝:Nata_ih✝:0+a✝=a✝⊢ 0+a✝.succ=a✝.succ<;>zero⊢ 0+zero=zerosucca✝:Nata_ih✝:0+a✝=a✝⊢ 0+a✝.succ=a✝.succsimp[*,add_zero,add_succ]All goals completed! 🐙theoremsucc_add(mn:Nat):succm+n=succ(m+n):=bym:Natn:Nat⊢ m.succ+n=(m+n).succinductionnzerom:Nat⊢ m.succ+zero=(m+zero).succsuccm:Nata✝:Nata_ih✝:m.succ+a✝=(m+a✝).succ⊢ m.succ+a✝.succ=(m+a✝.succ).succ<;>zerom:Nat⊢ m.succ+zero=(m+zero).succsuccm:Nata✝:Nata_ih✝:m.succ+a✝=(m+a✝).succ⊢ m.succ+a✝.succ=(m+a✝.succ).succsimp[*,add_zero,add_succ]All goals completed! 🐙theoremadd_comm(mn:Nat):m+n=n+m:=bym:Natn:Nat⊢ m+n=n+minductionnzerom:Nat⊢ m+zero=zero+msuccm:Nata✝:Nata_ih✝:m+a✝=a✝+m⊢ m+a✝.succ=a✝.succ+m<;>zerom:Nat⊢ m+zero=zero+msuccm:Nata✝:Nata_ih✝:m+a✝=a✝+m⊢ m+a✝.succ=a✝.succ+msimp[*,add_zero,add_succ,succ_add,zero_add]All goals completed! 🐙theoremadd_assoc(mnk:Nat):m+n+k=m+(n+k):=bym:Natn:Natk:Nat⊢ m+n+k=m+(n+k)inductionkzerom:Natn:Nat⊢ m+n+zero=m+(n+zero)succm:Natn:Nata✝:Nata_ih✝:m+n+a✝=m+(n+a✝)⊢ m+n+a✝.succ=m+(n+a✝.succ)<;>zerom:Natn:Nat⊢ m+n+zero=m+(n+zero)succm:Natn:Nata✝:Nata_ih✝:m+n+a✝=m+(n+a✝)⊢ m+n+a✝.succ=m+(n+a✝.succ)simp[*,add_zero,add_succ]All goals completed! 🐙
endHidden
The induction tactic also supports user-defined induction principles with
multiple targets (aka major premises). This example uses Nat.mod.inductionOn, which has the following signature:
We close this section with one last tactic that is designed to
facilitate working with inductive types, namely, the injection
tactic. By design, the elements of an inductive type are freely
generated, which is to say, the constructors are injective and have
disjoint ranges. The injection tactic is designed to make use of
this fact:
We are almost done describing the full range of inductive definitions
accepted by Lean. So far, you have seen that Lean allows you to
introduce inductive types with any number of recursive
constructors. In fact, a single inductive definition can introduce an
indexed family of inductive types, in a manner we now describe.
An inductive family is an indexed family of types defined by a
simultaneous induction of the following form:
In contrast to an ordinary inductive definition, which constructs an
element of some Sortu, the more general version constructs a
function ... → Sortu, where “...” denotes a sequence of
argument types, also known as indices. Each constructor then
constructs an element of some member of the family. One example is the
definition of Vectαn, the type of vectors of elements of α
of length n:
Notice that the cons constructor takes an element of
Vectαn and returns an element of Vectα(n+1), thereby using an
element of one member of the family to build an element of another.
A more exotic example is given by the definition of the equality type in Lean:
For each fixed α:Sortu and a:α, this definition
constructs a family of types Eqax, indexed by x : α.
Notably, however, there is only one constructor, refl, which
is an element of Eqaa.
Intuitively, the only way to construct a proof of Eqax
is to use reflexivity, in the case where x is a.
Note that Eqaa is the only inhabited type in the family of types
Eqax. The elimination principle generated by Lean is as follows:
It is a remarkable fact that all the basic axioms for equality follow
from the constructor, refl, and the eliminator, Eq.rec. The
definition of equality is atypical, however; see the discussion in Axiomatic Details.
The recursor Eq.rec is also used to define substitution:
Actually, Lean compiles the match expressions using a definition based on generated helpers
such as Eq.casesOn and Eq.ndrec, which are themselves defined using Eq.rec.
Using the recursor or match with h₁:a=b, we may assume a and b are the same,
in which case, pb and pa are the same.
It is not hard to prove that Eq is symmetric and transitive.
In the following example, we prove symm and leave as exercises the theorems trans and congr (congruence).
In the type theory literature, there are further generalizations of
inductive definitions, for example, the principles of
induction-recursion and induction-induction. These are not
supported by Lean.
We have described inductive types and their syntax through
examples. This section provides additional information for those
interested in the axiomatic foundations.
We have seen that the constructor to an inductive type takes
parameters—intuitively, the arguments that remain fixed
throughout the inductive construction—and indices, the arguments
parameterizing the family of types that is simultaneously under
construction. Each constructor should have a type, where the
argument types are built up from previously defined types, the
parameter and index types, and the inductive family currently being
defined. The requirement is that if the latter is present at all, it
occurs only strictly positively. This means simply that any argument
to the constructor in which it occurs is a dependent arrow type in which the
inductive type under definition occurs only as the resulting type,
where the indices are given in terms of constants and previous
arguments.
Since an inductive type lives in Sortu for some u, it is
reasonable to ask which universe levels u can be instantiated
to. Each constructor c in the definition of a family C of
inductive types is of the form
c : (a : α) → (b : β[a]) → C a p[a,b]
where a is a sequence of data type parameters, b is the
sequence of arguments to the constructors, and p[a, b] are the
indices, which determine which element of the inductive family the
construction inhabits. (Note that this description is somewhat
misleading, in that the arguments to the constructor can appear in any
order as long as the dependencies make sense.) The constraints on the
universe level of C fall into two cases, depending on whether or
not the inductive type is specified to land in Prop (that is,
Sort0).
Let us first consider the case where the inductive type is not
specified to land in Prop. Then the universe level u is
constrained to satisfy the following:
For each constructor c as above, and each βk[a] in the sequence β[a], if βk[a] : Sort v, we have u ≥ v.
In other words, the universe level u is required to be at least as
large as the universe level of each type that represents an argument
to a constructor.
When the inductive type is specified to land in Prop, there are no
constraints on the universe levels of the constructor arguments. But
these universe levels do have a bearing on the elimination
rule. Generally speaking, for an inductive type in Prop, the
motive of the elimination rule is required to be in Prop.
There is an exception to this last rule: we are allowed to eliminate
from an inductively defined Prop to an arbitrary Sort when
there is only one constructor and each constructor argument is either
in Prop or an index. The intuition is that in this case the
elimination does not make use of any information that is not already
given by the mere fact that the type of argument is inhabited. This
special case is known as singleton elimination.
We have already seen singleton elimination at play in applications of
Eq.rec, the eliminator for the inductively defined equality
type. We can use an element h:Eqab to cast an element
h₂:pa to pb even when pa and pb are arbitrary types,
because the cast does not produce new data; it only reinterprets the
data we already have. Singleton elimination is also used with
heterogeneous equality and well-founded recursion, which will be
discussed in a the chapter on induction and recursion.
We now consider two generalizations of inductive types that are often
useful, which Lean supports by “compiling” them down to the more
primitive kinds of inductive types described above. In other words,
Lean parses the more general definitions, defines auxiliary inductive
types based on them, and then uses the auxiliary types to define the
ones we really want. Lean's equation compiler, described in the next
chapter, is needed to make use of these types
effectively. Nonetheless, it makes sense to describe the declarations
here, because they are straightforward variations on ordinary
inductive definitions.
First, Lean supports mutually defined inductive types. The idea is
that we can define two (or more) inductive types at the same time,
where each one refers to the other(s).
In this example, two types are defined simultaneously: a natural
number n is Even if it is 0 or one more than an Odd
number, and Odd if it is one more than an Even number.
In the exercises below, you are asked to spell out the details.
A mutual inductive definition can also be used to define the notation
of a finite tree with nodes labelled by elements of α:
With this definition, one can construct an element of Treeα by
giving an element of α together with a list of subtrees, possibly
empty. The list of subtrees is represented by the type TreeListα,
which is defined to be either the empty list, nil, or the
cons of a tree and an element of TreeListα.
This definition is inconvenient to work with, however. It would be
much nicer if the list of subtrees were given by the type
List(Treeα), especially since Lean's library contains a number of functions
and theorems for working with lists. One can show that the type
TreeListα is isomorphic to List(Treeα), but translating
results back and forth along this isomorphism is tedious.
In fact, Lean allows us to define the inductive type we really want:
This is known as a nested inductive type. It falls outside the
strict specification of an inductive type given in the last section
because Tree does not occur strictly positively among the
arguments to mk, but, rather, nested inside the List type
constructor. Lean then automatically builds the
isomorphism between TreeListα and List(Treeα) in its kernel,
and defines the constructors for Tree in terms of the isomorphism.
Try defining other operations on the natural numbers, such as
multiplication, the predecessor function (with pred0=0),
truncated subtraction (with n-m=0 when m is greater
than or equal to n), and exponentiation. Then try proving some
of their basic properties, building on the theorems we have already
proved.
Since many of these are already defined in Lean's core library, you
should work within a namespace named Hidden, or something like
that, in order to avoid name clashes.
Define some operations on lists, like a length function or the
reverse function. Prove some properties, such as the following:
a. length(xs++ys)=lengthxs+lengthys
b. length(reversexs)=lengthxs
c. reverse(reversexs)=xs
Define an inductive data type consisting of terms built up from the following constructors:
Recursively define a function that evaluates any such term with respect to an assignment of values to the variables.
Similarly, define the type of propositional formulas, as well as
functions on the type of such formulas: an evaluation function,
functions that measure the complexity of a formula, and a function
that substitutes another formula for a given variable.