This part covers Lean both as a practical proof assistant and as the
dependent type theory it is built on. We begin with the natural
numbers -- a single type, dissected far enough to see how Lean
computes, together with the reduction rules behind that computation.
We then turn to types and the universe hierarchy -- including how to
define your own types with inductive and structure -- then
functions and terms and the typeclass system. Finally comes the
theory: the Curry-Howard correspondence tying propositions to types
and proofs to terms (and, with it, how to prove things interactively),
dependent types, and notation.
(Several reference topics -- the alphabetical tactic glossary, how a
Lean file is organized, Lean's brackets, equality and reduction, the
diagnostic commands, and Navigating Mathlib -- live in the
Appendix; the deeper foundations, the axioms of Lean
and well-foundedness, are treated in the Mathematics part.)
Before we start with more specific course content, we start by introducing the natural numbers, β (which is notation for the type Nat), in order to get things rolling and have a running example. Everything else in this part -- types, terms, proofs -- should be easier to read once you have seen how a single type is built and how Lean computes with it.
Nat is an inductive type: it is generated by a fixed list of constructors (the different possibilities to construct a natural number), the basic ways to build a value. Nat has exactly two (read a : b as a is a term of type b, and a β b as the type of maps from a to b, and succ means the successor of a natural number):
Nat.zero : Nat
Nat.succ : Nat β Nat
so every natural number is a finite tower of them: Nat.zero is 0, Nat.succ Nat.zero is 1, Nat.succ (Nat.succ Nat.zero) is 2, and so on. (The numerals 0, 1, 2 are just notation for these terms.) The general inductive declaration -- how you build your own types this way -- is the subject of a later section; here we only need Nat itself.
Paired with the constructors, and generated automatically alongside the type, is one further tool: the recursorNat.rec. Where the constructors build a value, the recursor consumes one -- it is the primitive way to define a function with domain Nat. To define some f : Nat β C (where C is some type), you supply exactly two things: a base value base : C for the zero case (i.e. f 0 := base, where f 0 is the Lean way to write f(0)), and a step : Nat β C β C such that, for succ n (which equals n + 1), you may reuse both n and the result already computed for n, i.e. you define in this way f (n + 1) := step n (f n). (Note the important notation here: We write Ξ± β Ξ² β Ξ³ -- with implicit brackets g : Ξ± β (Ξ² β Ξ³) for a function g mapping every a : Ξ± to a function Ξ² β Ξ³. So, g a b : Ξ³. In other words, this is the Lean and more flexible way to write a function depending on two variables.)
Noting the example keyword, as well as the inputs C, base, step, we define the terms of type Nat β C by introducing a function (using the fun keyword, and the mapping rule):
Pattern matching, and the cases and induction tactics, which we will encounter in the exercises and below, all compile down to Nat.rec. (The recursor has a still more general form, in which the result type may itself vary with the Nat, and which turns out to unify recursion with the induction principle. That needs the language of universes, so we return to it in the section on inductive types.)
What matters for the next sections is that Nat.reccomputes. Applied to a value that is literally one of the two constructors, it selects the matching case -- and the iota rule (ΞΉ) made precise below is applied (numerals such as 2 are put into constructor form first, so the rule applies to them too):
Nat.rec base step Nat.zero βΆ base
Nat.rec base step (Nat.succ n) βΆ step n (Nat.rec base step n)
For a full example, define "double" (note the keyword def at the beginning of the line, and the function name myDouble, the input n : Nat of the function, which returns a Nat) through the recursor and watch it reduce on 2 (here, _ is the name of a variable, which is not used):
The equality myDouble 2 = 4 needs no proof beyond rfl (which is the Lean way of saying that two thing are definitionally equal): both sides reduce to a common form. Naming those reduction steps precisely -- and pinning down when two terms are the same by computation -- is what the rest of this chapter is about.
As we just saw, terms in Lean reduce: myDouble 2 reduces to 4. A term reduces, by a fixed set of steps, toward a simpler form -- (fun x => x + 1) 4 becomes 5, and 2 + 2 becomes 4. Each such step is called a reduction and is written term βΆ simpler term. Two terms are definitionally equal -- also judgmentally equal, written a β‘ b -- when they reduce to a common form, and Lean's kernel then treats them as fully interchangeable, in particular when it compares types during type-checking. That comparison test is called conversion (or defeq checking). Definitional equality must be kept apart from propositional equalitya = b, which is itself a Prop and may require a genuine proof; the full three-way distinction -- syntactic, definitional, propositional -- is laid out in the appendix on equality.
The tactic rfl proves exactly a definitional equality. That is why goals whose two sides are written differently but reduce to the same thing close by rfl:
example:2+2=4:=rflexample:Nat.succ0=1:=rfl
The #eval command (used throughout the chapter on terms) runs the same reduction and prints the resulting value. So rfl, #eval, and every "reduces to" below are one and the same phenomenon. The precise steps are the reduction rules of the kernel, which we now name in full -- they recur throughout the course whenever a later step is claimed to hold "by definition".
Definitional equality is not magic -- it is generated by a fixed list of reduction rules that the kernel applies to compute terms. Most of the names come from the lambda calculus of Alonzo Church in the 1930s, where functions are written fun x => e and each rewriting rule carries a Greek letter. Lean's kernel uses six of them:
Rule
Technical name
What it does
Example
Ξ± (alpha)
Ξ±-conversion
rename a bound variable
fun x => x and fun y => y agree
Ξ² (beta)
Ξ²-reduction
apply a function: substitute the argument
(fun x => x + 1) 4 βΆ 4 + 1
Ξ· (eta)
Ξ·-conversion
a function equals its expansion fun x => f x
fun x => f x βΆ f
Ξ΄ (delta)
Ξ΄-reduction
unfold a definition: replace a constant by its value
twice 3 βΆ 2 * 3
ΞΉ (iota)
ΞΉ-reduction
run a recursor on a constructor
Nat.rec base step (n+1) βΆ step n (Nat.rec base step n)
ΞΆ (zeta)
ΞΆ-reduction
substitute a let-binding
let x := 4; x + x βΆ 4 + 4
Definitional equality -- Lean's notion of "the same by computation" -- is precisely the equivalence generated by these rules, and that is precisely what rfl checks: rfl proves a = b whenever a and b reduce to a common form. So rfl is far stronger than "syntactically identical":
example:(funx=>x+1)4=5:=rflexample:2+2=4:=rflexample:List.length[7,8,9]=3:=rfl
-- eta: a function equals its eta-expansion
example(f:NatβNat):(funx=>fx)=f:=rfl
-- zeta: a `let` unfolds
example:(letx:=4;x+x)=8:=rfl
-- delta + iota: a defined function on a literal
deftwice(n:Nat):Nat:=2*nexample:twice3=6:=rfl
Conversely, rflfails when no chain of reductions connects the two sides -- typically when a variable blocks computation. Nat's addition is defined by recursion on its second argument, so n + 0 fires the ΞΉ rule and reduces to n, whereas 0 + n is stuck: with n a bare variable there is no constructor for the recursor to fire on.
example(n:Nat):n+0=n:=rfl
Here, the red squiggly line indicates an error, i.e. a proof which does not work.
example(n:Nat):0+n=n:=Type mismatchrflhas type?m.9=?m.9but is expected to have type0+n=nrfl
To actually prove 0 + n = n you must run the recursor yourself, and for that you need induction -- the full Nat.rec, which hands you the induction hypothesis for the predecessor:
example(n:Nat):0+n=n:=n:ββ’ 0+n=ninductionnwithβ’ 0+0=0All goals completed! πk:βih:0+k=kβ’ 0+(k+1)=k+1
-- goal: 0 + (k + 1) = k + 1
-- ΞΉ reduces the LHS to succ (0 + k);
-- `Nat.add_succ` makes that step explicit,
-- then `ih : 0 + k = k` closes it.
All goals completed! π
(Here, rw [...] rewrites the goal using an equation; see the rw tactic.) Note that rw [ih]alone fails here: the goal reads 0 + (k + 1) = k + 1, and the subterm 0 + k is not yet visible -- it only appears after the ΞΉ step 0 + succ k βΆ succ (0 + k), which Nat.add_succ performs. This is the same stuck-until-a-constructor-shows-up phenomenon one level down.
Recognizing when a goal is "true by reduction" -- and therefore closed by rfl -- is one of the most useful instincts to develop; it is exactly the situation later chapters rely on whenever they say a step holds "by definition" or "definitionally".
The ΞΉ rule is the one we met concretely with Nat in the previous section: a recursor is applied to a value that is literally a constructor, selecting the matching case (Nat.rec base step (Nat.succ n) βΆ step n (Nat.rec base step n)). It is the rule that makes every recursive definition reduce -- and its getting stuck, when the recursed-on argument is still a variable rather than a constructor, is exactly what blocks 0 + n above.
Two further rules: proof irrelevance and quotient computation
Beyond the six lambda-calculus rules, the kernel's definitional equality carries two further rules specific to Lean's foundations:
Proof irrelevance: any two proofs hβ hβ : P of the same proposition P : Prop are definitionally equal. (This is the Prop is special point, taken up in why Prop is special.)
Quotient computation: the ΞΉ-like rule for quotient types, Quot.lift f h (Quot.mk r a) βΆ f a. On Nat, a lifted function applied to a Quot.mk reduces to the underlying function:
Reducibility, weak head normal form, and decidability
How eagerly the Ξ΄ rule fires is governed by reducibility annotations: a @[reducible] definition is unfolded freely, an ordinary def is unfolded on demand, and an @[irreducible] one is left folded. And the kernel usually does not reduce all the way to a normal form; it stops at weak head normal form (WHNF) -- just far enough to expose the outermost constructor or binder -- which is all that comparing two terms requires. Because this reduction system is confluent and strongly normalizing, definitional equality is decidable, which is exactly what lets rfl and the type-checker terminate.
The kernel is the small, trusted core of Lean that has the final say on whether a term has the type it claims. Everything else you write -- tactics, notation, match, coercions, [β¦] instance holes -- is handled outside the kernel by the elaborator: the elaborator takes your surface syntax and turns it into a single, fully explicit term, which the kernel then re-checks from scratch against a fixed, tiny set of rules (the reduction rules, the typing rules, and the primitive universes Sort u). A handful of things are built into this core rather than defined on top of it: the universes Prop and Type u, the way an inductive type generates its constructors and recursor, and proof irrelevance. The point of keeping the kernel small is trust -- to believe a Lean proof you need only believe the kernel, not the large machinery of tactics and automation above it. That trust is reinforced by there being several independent implementations of the kernel: besides Lean's own reference kernel (written in C++), the same proof terms can be re-checked by separate type-checkers written in other languages, and even by one (Lean4Lean) written and formally verified in Lean itself. A proof accepted by one kernel can thus be cross-checked by another. The kernel's logic has a name, the Calculus of Inductive Constructions, discussed in the Mathematics part.
There are inductive types even simpler than Nat. One of them, Bool, is the type with two values, which are called true and false. It has exactly two constructors, neither taking any argument. Here is its definition, straight from Lean's core library (the where keyword introduces the list of constructors):
Logically speaking, this is equivalent to Prop (the type of propositions). The distinction is
public important for programming: both propositions and their proofs are erased in the code generator,
while Bool corresponds to the Boolean type in most programming languages and carries precisely one
bit of run-time information.
The Boolean value true, not to be confused with the proposition True.
Its recursor Bool.rec consumes a Bool by offering one value for each constructor -- first the false case, then the true case -- and returns the matching one. Into a fixed result C : Type, its (non-dependent) signature is just this, exactly as Nat.rec above:
-- the `false` case first, then the `true` case:
Bool.rec : C β C β Bool β C
This is exactly an if-then-else: reading Bool.rec e f b as "if b then f else e" (note the order: the false result e comes first). Applied to a literal constructor it reduces by the ΞΉ rule (the reduction rules below). Equivalently (and definitionally equal) is the following construction (read bif as Boolean if):
example:(biftruethen"Hi"else"Salut")="Hi":=rfl
-- and in general `bif b then f else e` is `Bool.rec e f b`:
example{Ξ±:Type}(ef:Ξ±):(biffalsethenfelsee)=Bool.receffalse:=rfl
Note, as above in the 0 + n = n example, the bif fails to reduce if the value of b is unknown:
example(b:Bool):(bifbthen"Hi"else"Salut")=Bool.rec"Salut""Hi"b:=b:Boolβ’ (bifbthen"Hi"else"Salut")=Bool.rec"Salut""Hi"bTactic `rfl` failed: The left-hand sidebifbthen"Hi"else"Salut"is not definitionally equal to the right-hand sideBool.rec"Salut""Hi"bb:Boolβ’ (bifbthen"Hi"else"Salut")=Bool.rec"Salut""Hi"bb:Boolβ’ (bifbthen"Hi"else"Salut")=Bool.rec"Salut""Hi"b
The fix is to make a case analysis as follows (cases is a simpler form of induction; see cases and induction):
Internally, Lean does not build runnable definitions from Bool.rec directly -- the code generator cannot compile a bare recursor, whereas it can compile a match. So bif is not primitive: it is notation for a function cond, which does the actual branching with a match:
-- the underlying function, defined by matching on `c`:
def cond {Ξ± : Type} (c : Bool) (x y : Ξ±) : Ξ± :=
match c with
| true => x
| false => y
-- `bif` is *notation* that expands to a `cond` call:
macro "bif " c:term " then " t:term " else " e:term :
term => `(cond $c $t $e)
Both types in this section live in Prop, the universe of propositions. Before defining them, a word on Prop itself. Unlike Bool, Nat, or the True and False below -- each an inductive type whose definition you can read off -- Prop has no such definition. It is not an inductive type, and there is no def Prop := β¦ to unfold; asking Lean to print it (#print Prop) even fails, because there is nothing written in Lean to print. Instead, Prop is a primitive, baked directly into Lean's kernel: it is the bottom universe Sort 0 (with Type u being Sort (u+1); more on the universe hierarchy in the next chapter). What makes Prop usable is therefore not a definition but the rules the kernel attaches to it -- proof irrelevance and the rest, taken up in why Prop is special.
The trivial proposition True.True is the proposition that always holds. Being a Prop, its terms are proofs. It has a single constructor, taking no argument, which is therefore already a complete proof of it (note that the type after the colon is now Prop, not Type):
Its recursor has exactly one case, for the single constructor True.intro, so into a fixed result type C its (non-dependent) signature is:
-- one constructor, hence one case:
True.rec : C β True β C
There is correspondingly nothing to learn from a proof of True: the recursor just hands back the value you supplied for that one case (True.rec c h reduces to c), which is why True carries no information.
The empty proposition False.False is the opposite extreme: the proposition that is never true. It is the inductive type with no constructors at all -- so its definition has no where clause and no | lines:
False is the empty proposition. Thus, it has no introduction rules.
It represents a contradiction. False elimination rule, False.rec,
expresses the fact that anything follows from a contradiction.
This rule is sometimes called ex falso (short for ex falso sequitur quodlibet),
or the principle of explosion.
For more information: Propositional Logic
Constructors
Since there is no constructor, there is no way to build a term of False -- exactly as it should be for a statement that has no proof. Its recursor is the striking one, and here we give its full signature (the result universe Sort u is treated in the next chapter):
Implicit vs. explicit motive: why False.rec looks different
This looks different from Bool.rec and True.rec above for one reason -- implicit versus explicit: there the motive sat in curly braces {motive β¦} and Lean inferred it from the case values, which is why we could drop it and even present those signatures non-dependently as C β C β Bool β C and C β True β C. False has no case values, so nothing lets Lean infer the motive; it is therefore explicit (round brackets (motive β¦)), and you must supply it -- you write False.rec (fun _ => C) h, or False.rec _ h letting the goal fix it, but never plain False.rec h (which would read h as the motive). Note also that between the motive and the input t there is no case argument at all -- Bool.rec had two there, True.rec one, False.rec none -- which is exactly why its result motive t may be of any type: the principle ex falso quodlibet ("from a falsehood, anything follows"). The filled-in form is packaged as False.elim, so that False.elim h = False.rec (fun _ => C) h.
The very same split appears one universe down, in Type -- which shows that it turns on the number of constructors, not on Prop. The Type-level twins of True and False are Unit (one constructor, written ()) and Empty (no constructor at all):
-- one constructor β motive implicit, as for `True.rec`
PUnit.rec : {motive β¦} β motive PUnit.unit β
(t : PUnit) β motive t
-- no constructor β motive explicit, as for `False.rec`
Empty.rec : (motive : Empty β Sort u) β
(t : Empty) β motive t
So Empty.rec is the data version of False.rec -- explicit motive, no case argument -- and its packaged form Empty.elim : Empty β C is the Type-level ex falso, the exact twin of False.elim. (One wrinkle: Unit is a reducible abbreviation for PUnit, so its recursor is called PUnit.rec, not Unit.rec.) Whether the motive is implicit or explicit is therefore settled solely by whether there is a case value to infer it from; Prop versus Type never enters in.
For ex falso quodlibet, Lean does not use False.rec but False.elim, which is the following:
def False.elim {C : Sort u} (h : False) : C :=
False.rec (fun _ => C) h
This is exactly what makes False the yardstick of consistency. Because False.elim turns a single proof of False into a proof of every proposition P (the second example above), one term of type False would make every statement a theorem at once -- 0 = 1, its negation, everything -- and the logic would no longer distinguish the provable from the false. A system in which False is inhabited therefore proves nothing of value; it is contradictory. So the entire point of the type theory is arranged around a single requirement: that no term of False can ever be built. Whenever we later call a construction consistent, or note that Lean rejects Type : Type, or that a tactic is sound, it comes back to this -- False must stay empty.
-- We meet False again as the reason a contradiction closes any goal, and all three types return in the discussion of why Prop is special, where False's empty elimination and Bool's two-way split are exactly what mark the line between data and proof.
In all programming languages, you have data types such as int, string and float. In Lean, these exist as well, but you can (and will in this course) define own data types. In all cases, we write x : Ξ± for a term x of type Ξ±, so we write False : Bool, 42 : β, but also f : β β β (for a function from β to β, which is an own type) and 0 β 1 : Prop (the proposition that 0 and 1 are different natural numbers), which is a proposition. Terms and types can depend on variables, e.g. in β (n : β), n < n + 1 : Prop (the term n < n + 1 depends on n : β) and f : (n : β) β (Fin n β β) where Fin n is the type which carries {0, ..., n-1} (here, the type Fin n β β depends on n : β), which is a function f with domain β such that f n β β^n.
Two words for terms recur throughout, depending on their type: a term h : P whose type P is a Prop is called a proof (of P), while a term a : Ξ± whose type Ξ± is a Type u (where u will be called a universe) is called data. So 42 : β and true : Bool are data, whereas any term of the proposition 0 β 1 is a proof of it. (The two kinds of universe, Prop and Type u, are the subject of the next section.)
As we see, these new data types are more abstract: Lean understands β (and β) as genuinely infinite types, not limited by floating-point arithmetic. The zero/succ construction of β was the subject of the introduction; the real numbers, by contrast, are built from an equivalence relation on Cauchy sequences, which is considerably more elaborate -- a quotient type, as we will see at the end of this chapter.
In Lean, all objects are terms, and every term needs a type. Interestingly, since a type is also some term in the language, it needs a type as well.
The answer is a hierarchy: these types-of-types (called universes) are organized into a countably infinite tower, which is exactly what keeps the system consistent.
At the bottom sit the two universes you meet first (Here, the #check command gives the type of a term):
42 : β#check(42:β) -- a term ...
β : Type#check(β:Type) -- ... whose type β lives in `Type`
2=2 : Prop#check((2=2):Prop) -- a proposition ...
Prop : Type#check(Prop:Type) -- ... and `Prop` itself lives in `Type`
Type (which is the same as Type 0) is the universe of ordinary data types (β, β, Bool, List Ξ±, ...), and Prop (which is the universe of propositions, i.e. True/False-statements). But Type cannot contain itself -- that would be paradoxical -- so Type in turn lives in a larger universe Type 1, which lives in Type 2, and so on without end:
Type : Type 1#check(Type:Type1) -- Type : Type 1
Type 1 : Type 2#check(Type1:Type2) -- Type 1 : Type 2
The connection of Type to Sort
The whole tower is captured by a single keyword Sort:
So Sort is the umbrella that unifies Prop and all the Type u, and the one rule governing the hierarchy is Sort u : Sort (u+1). There is deliberately no Type : Type; why this restriction is needed -- it blocks a type-theoretic version of Russell's paradox -- is taken up in the Mathematics part.
1.2.2.Β How the universe of a type is determinedπ
You rarely write universe levels by hand -- Lean computes the universe of a compound type from the universes of its parts. A function type Ξ± β Ξ² lands in the larger of the two universes involved:
βββ : Type#check(βββ) -- Type
ββType : Type 1#check(ββType) -- Type 1 (because `Type : Type 1`)
The same holds for a β (a dependent function type), whose universe is read off from the universe it ranges over together with that of its body. When the body is data -- something in a Type -- the whole β is forced to grow with the domain:
(Ξ±:Type)βΞ±βΞ± : Type 1#check(βΞ±:Type,Ξ±βΞ±) -- Type 1
(Ξ±:Type 5)βΞ±βΞ± : Type 6#check(βΞ±:Type5,Ξ±βΞ±) -- Type 6
This is not mere bookkeeping. Suppose β Ξ± : Type, Ξ± β Ξ± were allowed to have type Type rather than Type 1 -- i.e. a β over Type could land back inside Type. That is the first step toward a universe that contains itself, Type : Type, and that is fatal: from Type : Type one can encode Russell's paradox at the level of types -- Girard's paradox -- and derive an honest proof of False, so every proposition becomes provable and the system is worthless. Predicativity, by forcing the climb to Type 1, is exactly what rules this out. (The proof is not short, and Lean's kernel enforces the bump, so you cannot even state β Ξ± : Type, Ξ± β Ξ± : Type to try it; the construction of the paradox from Type : Type is spelled out in the Mathematics part.)
(A definition can also be made to work at any universe level at once; that uses def, so we defer it to the chapter on functions.)
There is exactly one universe that is allowed to break this rule -- Prop -- and that exception is the subject of the next section.
Of particular interest is the type Prop, which consists of statements that can be True or False. It includes mathematical statements, so either the hypotheses, or the goal of what is to be proven. A hypothesis in Lean has the form hP : P, which means P is true, and this statement is called hP. Synonomously, it means that P is true and hP is a proof of P. The hypotheses here have names P Q R S, and the proofs of the hypotheses hP hQ hR hS. All names can be arbitrary. Furthermore, there are hypotheses of the form P β Q, which is the statement that P implies Q. (Note the similarity to function notation as in f : β β β.)
We note three specifics which only apply to Prop:
Proof irrelevance: Note that Prop only records that a statement holds, but not which proof we chose. This is proof irrelevance, which means the following goal closes by rfl:
example(P:Prop)(hβhβ:P):hβ=hβ:=rfl
For data living in a Type there is no such collapse, for obvious reasons.
example(ab:β):a=b:=Type mismatchrflhas type?m.3=?m.3but is expected to have typea=brfl
Prop is impredicative: Prop is the one universe that escapes the predicativity of the previous section. As long as the body of a β statement is a proposition, the whole β is a Prop -- even when we range over an arbitrarily large universe of types:
Compare this with the Type-valued β Ξ± : Type 5, Ξ± β Ξ±, which had to climb to Type 6. The two statements are syntactically parallel and differ only in their body, yet land in wildly different places -- Prop versus Type 6. A β into Prop stays small; a β into Type u must climb. This is exactly what impredicative means: a proposition may be defined by quantifying over a totality to which it itself belongs. β P : Prop, P is again a Prop, so it ranges over a collection that already contains it -- the definition, so to speak, feeds on itself. Prop is allowed this self-reference because proof irrelevance keeps it harmless.
Restricted (subsingleton) elimination: First, a word on what elimination means. A type's constructors introduce its values -- they are the ways we build them (Or.inl, isTrue, Nat.succ, and so on). Eliminating a value is the opposite: we use it, by looking at which constructor produced it. This is what a match does -- the pattern-matching construct match h with | β¦ => β¦, taken up properly in the chapter on terms -- and, as we saw in the introduction, it is ultimately the job of the type's recursor. For a propositionP : Prop, then, eliminating a proof means running P's own recursor -- Or.rec, And.rec, and the like (the error below names Or.casesOn, the match-friendly form of Or.rec) -- and the restriction we are about to meet is a restriction on exactly that recursor. To eliminate into a type T means that this case analysis returns a result of type T; eliminating into T, defining a function into T by the recursor, and letting that recursor compute are three names for one act. The restriction on Prop is about exactly this. A proof carries no observable content, so Lean does not let us read data off a proof in this way: otherwise the result could depend on which proof we started from, and proof irrelevance says there is no such difference to observe. So eliminating a proposition may, in general, only produce further propositions, never data. For example, deciding as a Bool which side of a disjunction holds is rejected:
example(ab:Prop)(h:aβ¨b):Bool:=Tactic `cases` failed with a nested error:Tactic `induction` failed: recursor `Or.casesOn` can only eliminate into `Prop`aΒ b:Propmotive:aβ¨bβSort ?u.43h_1:(h:a)βmotiveβ―h_2:(h:b)βmotiveβ―hβ:aβ¨bβ’ motivehβ after processing_the dependent pattern matcher can solve the following kinds of equations
- <var> = <term> and <term> = <var>
- <term> = <term> where the terms are definitionally equal
- <constructor> = <constructor>, examples: List.cons x xs = List.cons y ys, and List.cons x xs = List.nilmatchhwith|Or.inl_=>true|Or.inr_=>false
The error message is telling: recursor 'Or.casesOn' can only eliminate into 'Prop'. There is one exception, called subsingleton elimination. If a proposition has at most one constructor and all of its fields are themselves proofs, then it has at most one inhabitant, so eliminating it can reveal nothing -- and Lean does allow it into any type. This covers False (no constructors at all, which is exactly why False.elim closes any goal), Eq (which is why we may rw even in goals that carry data), and And (both of its fields are proofs). It does not cover Or (the disjunctionβ¨, which has two constructors) or β (its first field is a genuine witness, not a proof). None of this applies to Type: an inductive type in Type always eliminates into anything.
Why must it be this way? This is not red tape -- consistency forces it, and for Or the reason is worth spelling out. What cannot escape here is not a witness (as it would be for β) but the tag: the information about which of the two branches holds. Take a true a and consider the proposition a β¨ a. Its two constructors give definitionally equal terms, by proof irrelevance:
Here h : a is a proof of the (true) proposition a. "Left or right?" is simply not a well-posed question about a proof of a β¨ a: the tag is not distinguishable content. So if the rejected (a β¨ b) β Bool above were allowed -- call the resulting function which -- then setting b := a would give which (Or.inl h) = true and which (Or.inr h) = false; but Or.inl h β‘ Or.inr h (they are definitionally equal, the β‘ from the previous chapter), so we would get true β‘ false, a contradiction. Because proofs are indistinguishable, the tag has to stay trapped -- just as a leaked β-witness would have forced 3 β‘ 5.
We cannot build such a which -- Lean rejects it -- but we can assume one exists for everya β¨ b (with its left- and right-branch computation rules as hypotheses) and then instantiate at b := a, where the two injections collide. Watch true = false -- hence False -- drop out:
example(which:β{ab:Prop},aβ¨bβBool)(left:β{ab:Prop}(h:a),which(Or.inlh:aβ¨b)=true)(right:β{ab:Prop}(h:a),which(Or.inrh:bβ¨a)=false):False:=which:{ab:Prop}βaβ¨bβBoolleft:β{ab:Prop}(h:a),whichβ―=trueright:β{ab:Prop}(h:a),whichβ―=falseβ’ False
-- at `a, b := True`, the injections collide:
havehtf:true=false:=which:{ab:Prop}βaβ¨bβBoolleft:β{ab:Prop}(h:a),whichβ―=trueright:β{ab:Prop}(h:a),whichβ―=falseβ’ FalseAll goals completed! πwhich:{ab:Prop}βaβ¨bβBoolleft:β{ab:Prop}(h:a),whichβ―=trueright:β{ab:Prop}(h:a),whichβ―=falseβ’ true=falseβFalseAll goals completed! π
The way out mirrors the one for β, one level up. The data-carrying counterpart of a β¨ Β¬a is Decidable a (the Decidable typeclass), which -- crucially -- lives in Type, not Prop (its declaration uses the constructor-list where from the Bool section):
Either a proof that p is true or a proof that p is false. This is equivalent to a Bool paired
with a proof that the Bool is true if and only if p is true.
Decidable instances are primarily used via if-expressions and the tactic decide. In
conditional expressions, the Decidable instance for the proposition is used to select a branch. At
run time, this case distinction code is identical to that which would be generated for a
Bool-based conditional. In proofs, the tactic decide synthesizes an instance of Decidablep,
attempts to reduce it to isTrueh, and then succeeds with the proof h if it can.
Because Decidable carries data, when writing @[simp] lemmas which include a Decidable instance
on the LHS, it is best to use {_:Decidablep} rather than [Decidablep] so that non-canonical
instances can be found via unification rather than instance synthesis.
Proves that p is decidable by supplying a proof of p
Because Decidable p is in Type, it may eliminate into Type -- which is exactly why if h : p then _ else _ (a dependentif, whose h : p names the proof made available in the then-branch) and the decide tactic compute. The two stories line up exactly:
The classical route mirrors Classical.choose in the same way: Classical.em : a β¨ Β¬a is always available (it is a Prop), but its data-carrying counterpart Classical.propDecidable : Decidable a goes through Classical.choice and is therefore noncomputable -- a keyword marking a definition Lean cannot turn into runnable code, needed here because Classical.choice has no computational content to execute (this is the usual trigger for noncomputable, though not the only one). So the two halves -- β/Ξ£/choose for the witness, β¨/Decidable/em for the tag -- are really the same story: computationally relevant information can leave the Prop world only if we put it in Type from the start, or pay for it noncomputably with the axiom of choice. (The Nonempty/Classical.choice form of this is taken up in the chapter on propositions and proofs.)
Many everyday types in Lean -- Nat, List, Option, Bool, even Empty -- are inductive types. We have already dissected two of them, Nat and Bool, in the previous chapter. This section is about the general mechanism -- how you declare such a type yourself, and which declarations Lean accepts.
You declare an inductive type by giving a name, the type's universe, and a list of constructors, each saying how to build a new element out of existing pieces. Declaring the natural numbers by hand reproduces exactly the structure of Nat:
As with Nat, this single declaration introduces three things at once: the type MyNat; its two constructors MyNat.zero and MyNat.succ, so every element is either zero or succ n; and a recursorMyNat.rec -- the type's eliminator, the counterpart of its constructors -- of the same shape as Nat.rec, into which pattern matching, cases, and induction all translate. Its iota rule, MyNat.rec z s (.succ n) βΆ s n (MyNat.rec z s n), is the firing we watched for Nat. And a recursor is not special to inductive: since a structure is a single-constructor inductive, it too has one -- Point.rec (from the next section) takes a single case, a function of the fields, and the projections Point.x, Point.y are defined through it.
Now that we have universes in hand, we can state the recursor in full generality. The introduction used its non-dependent form, into a fixed result type; the true MyNat.rec lets that type depend on the value being consumed, a dependency recorded by a motiveMyNat β Sort u (the leading @ in @MyNat.rec makes Lean's normally-hidden implicit arguments -- here the motive -- explicit, so that #check prints them; see the appendix on parentheses):
@MyNat.rec : {motive:MyNatβSort u_1}βmotiveMyNat.zeroβ((n:MyNat)βmotivenβmotiven.succ)β(t:MyNat)βmotivet#check@MyNat.rec
-- {motive : MyNat β Sort u} β
-- motive .zero β -- zero case
-- ((n : MyNat) β motive n β
-- motive n.succ) β -- succ case
-- (t : MyNat) β motive t
With a data-valued motive this is the recursion the introduction showed; with a Prop-valued one it is precisely the induction principle. Recursion and induction are thus two readings of the single primitive MyNat.rec -- which is why the induction tactic and recursive definitions feel so alike.
The declaration only forms the type. How to actually build its elements and define functions on it -- typically by pattern matching on the constructors -- is the subject of the next chapter, on constructing terms.
Proofs about an inductive type use the induction tactic, which applies the recursor for you: one subgoal per constructor, with an induction hypothesis for each recursive argument.
Inductive types are the main mechanism by which new data types enter Lean; Mathlib uses them extensively, and understanding them is essential for reading the library. This also answers the question of the previous section from the other side: the universe of an inductive type must be large enough to hold all of its constructor arguments.
Not every inductive declaration is accepted, though. A constructor may mention the type being defined, but only positively -- never to the left of an arrow inside one of its arguments. This strict positivity condition keeps inductive types built from well-founded data and free of paradox. An infinitely-branching tree -- where a node carries one subtree for each natural number -- is fine, because in the node constructor MyTree occurs only to the right of β:
inductiveMyTreewhere|leaf:ββMyTree -- a leaf holds a value
|node:(ββMyTree)βMyTree -- positive occurrence
-- leaves now carry data, and trees can be built:
example:MyTree:=.leaf7example:MyTree:=.node(funn=>.leafn)
The leaf case is what gets the type off the ground, and it is worth pausing on why it is needed. Positivity constrains only whereMyTree may occur, not whether the type has any elements: drop leaf, and the declaration is still accepted, but no closed MyTree can ever be built -- every node already demands a whole family β β MyTree of subtrees, with nothing to start from, so the type is empty (like Empty; one can even prove MyTree β False). With leaf present you build upward -- leaf 7, then node (fun n => leaf n), then trees over those. Note that leaf here carries a β, so different leaves are genuinely different; a bare leaf : MyTree would give a single, featureless leaf, indistinguishable from every other.
A word on the shape, too. Because node takes a functionβ β MyTree, every node has one child per natural number -- it branches infinitely. It is not that everyMyTree is infinite: a leaf is finite (leaf 7 is a complete tree), so only trees that actually use node are. This infinite branching is inseparable from a function-typed argument; for a finitely-branching tree you would instead write something like node : MyTree β MyTree β MyTree, but there MyTree occurs directly, not inside a function, so the right-of-β subtlety we are illustrating would not even arise.
But a constructor that stores a function out of the type is rejected:
inductive Bad where
| mk : (Bad β False) β Bad
-- error: (kernel) arg #1 of 'Bad.mk' has a non positive
-- occurrence of the datatypes being declared
The rejection is not pedantry: were Bad allowed, one could prove False. Project the stored function out of a b : Bad and apply it to b itself,
def app : Bad β (Bad β False) | .mk f => f
def neg (b : Bad) : False := app b b
While inductive types let us define types with multiple constructors, many mathematical objects are better described as a collection of named fields. For example, a point in the plane has an x-coordinate and a y-coordinate. In Lean, we use structure for this.
A structure is a special case of an inductive type with exactly one constructor and named fields. Here is a simple example:
structurePointwherex:βy:β
This declares a new type Point whose elements are records with two β fields. (Both must be given in order to define a Point, which is the only way we can make such a Point, i.e. it only has a single constructor.) Like every declaration, it produces more than the type alone: a constructor Point.mk and one projection per field. We can inspect their types without building any value yet:
so a Point3D has fields x, y (from Point) and z. This is particularly important in Mathlib, where the algebraic hierarchy uses structure extension extensively: CommRing extends Ring, which extends Semiring, and so on.
Structures are natural for representing mathematical objects. A Gaussian integer (a complex number with integer parts) is a pair of β€s,
structureMyComplexwherere:β€im:β€
and a structure may bundle data together with a property -- here a linear map, carrying both a function and a proof that it respects addition (the square brackets [Add Ξ±] are an instance-implicit argument, requiring Ξ± to carry an addition; see the typeclasses chapter):
Use structure when your type has a single constructor with named fields. Examples: points, complex numbers, algebraic structures.
Use inductive when your type has multiple constructors. Examples: natural numbers, lists, trees, Option, Bool.
A structure is syntactic sugar for an inductive type with one constructor. The definition
structure Point where
x : β
y : β
is essentially equivalent to
inductive Point where
| mk : β β β β Point
but the structure version gives us named fields, dot notation, the possibility for default values, and the extends mechanism. (A class is in turn a structure marked for use by instance search; we return to it in the Typeclass chapter.)
Alongside universes, function types, and inductive types, Lean has one more basic way to form a type: the quotient. Given a type Ξ± and a relation r : Ξ± β Ξ± β Prop, the type Quot r glues together elements that r relates -- it is Ξ± "seen up to r". This is how the chapter's opening remark, that β is Cauchy sequences up to an equivalence, becomes an actual construction; β€, β, Multiset Ξ± (lists up to reordering), and ZMod n are quotients too.
Its entire interface is four constants:
@Quot.mk : {Ξ±:Sort u_1}β(r:Ξ±βΞ±βProp)βΞ±βQuotr#check@Quot.mk -- (r : Ξ± β Ξ± β Prop) β Ξ± β Quot r
@Quot.sound : β{Ξ±:Sort u_1}{r:Ξ±βΞ±βProp}{ab:Ξ±},rabβQuot.mkra=Quot.mkrb#check@Quot.sound -- r a b β Quot.mk r a = Quot.mk r b
@Quot.lift : {Ξ±:Sort u_1}β{r:Ξ±βΞ±βProp}β{Ξ²:Sort u_2}β(f:Ξ±βΞ²)β(β(ab:Ξ±),rabβfa=fb)βQuotrβΞ²#check@Quot.lift -- (f : Ξ± β Ξ²) β (β a b, r a b β f a = f b) β Quot r β Ξ²
@Quot.ind : β{Ξ±:Sort u_1}{r:Ξ±βΞ±βProp}{Ξ²:QuotrβProp},(β(a:Ξ±),Ξ²(Quot.mkra))ββ(q:Quotr),Ξ²q#check@Quot.ind
Quot.mk r sends each element to its class.
Quot.sound is the crux: if r a b, then the two classes are the same term. It is one of Lean's few genuine axioms -- the #print axioms name command traces a constant back to the axioms it depends on, and here #print axioms Quot.sound reports [Quot.sound].
Quot.lift defines a function out of the quotient -- but only once you prove it respects r (the β a b, r a b β f a = f b argument), which is exactly the mathematician's "check that this is well-defined". And it computes: Quot.lift f h (Quot.mk r a) reduces to f a.
Quot.ind proves a property of every quotient element by checking it on the classes Quot.mk r a.
In practice one usually goes through Quotient, a thin wrapper of Quot over a bundled equivalence relation (a Setoid). The status of Quot.sound as an axiom is taken up again in the axioms chapter.
How β is built as a quotient
Mathlib's real numbers are a concrete instance of exactly this construction. First, a CauSeq is a subtype: the notation { f : β β β // IsCauSeq abs f } means a rational sequence f : β β β bundled with a proof that it is Cauchy.
CauSeqβabs : Type#check(CauSeqβabs)
-- CauSeq β abs = { f : β β β // IsCauSeq abs f }
example(f:CauSeqβabs):βββ:=f.1
β is then a one-field structure wrapping the quotient of these sequences by the relation "the difference tends to 0". The class map CauSeq.Completion.mk is the Quot.mk of that quotient:
Real.ofCauchy : CauSeq.Completion.Cauchyabsββ#check@Real.ofCauchy
-- CauSeq.Completion.Cauchy abs β β
@CauSeq.Completion.mk : {Ξ±:Type u_1}β[inst:FieldΞ±]β[inst_1:LinearOrderΞ±]β[inst_2:IsStrictOrderedRingΞ±]β{Ξ²:Type u_2}β[inst_3:RingΞ²]β{abv:Ξ²βΞ±}β[inst_4:IsAbsoluteValueabv]βCauSeqΞ²abvβCauSeq.Completion.Cauchyabv#check@CauSeq.Completion.mk
-- CauSeq β abs β CauSeq.Completion.Cauchy abs
-- two sequences give the same real iff (f - g) β 0;
-- `β` is the Setoid equivalence relation on `CauSeq`
example(fg:CauSeqβabs):fβgβ(f-g).LimZero:=Iff.rfl
Addition is the payoff of Quot.lift. To add two reals you add representative sequences termwise -- but this is only well-defined because + respects the relation (if f β f' and g β g' then f + g β f' + g'). Having checked that, Quot.lift hands you exactly the computation rule that mk is an additive homomorphism:
This mk (f) + mk (g) = mk (f + g) equation is the Quot.lift f h (Quot.mk r a) βΆ f a reduction from the reduction rules, applied to a two-argument operation.
1.3.2.Β Constructing and using structure valuesπ
We read fields back with dot notation. Here the #eval command evaluates a (computable) term and prints the resulting value -- the same reduction that rfl performs, only with the answer displayed:
Since structures are immutable (as everything in functional programming), this creates a new Point rather than modifying p1. You may even draw from several records at once -- { p, q with β¦ } takes each remaining field from the first of p, q that supplies it. When a structure declares field defaults, a value may omit those fields:
p2.normSq (the squared distance from the origin) works because Lean sees that p2 : Point and looks for Point.normSq. The Gaussian-integer type gives a fuller example -- data and its operations together:
A function is again just a term -- one whose type is a function
type. We define one with def, now writing its arguments before
the :=, and evaluate it with #eval. The shape is
def name (arguments) : resultType := body
so the following defines double, taking one argument n : β and
returning the β value 2 * n:
Application is written f x (no parentheses -- f x is Lean's way
of writing f(x)). A pure function returns the same result for
the same arguments, exactly as in mathematics.
Functions of several arguments are curried: a function of two
arguments is really a function that takes one argument and returns
another function.
Functions can take types as arguments, not just values -- this is how
you write a single definition that works for every type. The identity
function is the basic example:
Usually you do not want to pass the type explicitly every time, so you
make it an implicit argument with braces {...}; Lean then infers it
from the value:
A definition that should also work at any universe level -- recall
the universe hierarchy -- uses a universe
variable. You may introduce one with the universe command, or, most
commonly, write Type*, which inserts a fresh universe variable for
you:
This is why signatures throughout Mathlib read {Ξ± : Type*}: the same
definition then applies whether Ξ± is small like β or large like
Type itself. We use {Ξ± : Type*} freely in the examples below.
In mathematics one would write x β¦ x^2; in Lean we write
fun x β¦ x ^ 2. There is an even shorter form using the Β·
placeholder (a centered dot, typed \cdot): a Β· stands for an
anonymous argument, so (Β· + 10) is shorthand for fun x β¦ x + 10,
and several Β· become successive arguments -- (Β· + Β·) means
fun x y β¦ x + y.
For functions on inductive types (like β, List Ξ±, Option Ξ±,
Bool), the most natural way to define them is by pattern matching
on the constructors of the input. The syntax uses match ... with
and one branch per constructor, each prefixed by a |.
For example, the factorial function on β matches on whether the
input is 0 or a successor n + 1:
Each branch may use the variables introduced by its pattern (here
n on the right-hand side). Lean checks two things automatically:
Exhaustiveness. Every constructor of β is covered (the cases
0 and n + 1 exhaust β). If you forget a case, Lean
complains.
Termination. The recursive call factorial n is on a strictly
smaller argument than n + 1, so the definition is well-founded.
It works just as well for the inductive types you declare yourself. For MyNat from the previous chapter, doubling is defined by matching on the two constructors:
The leading dot in .zero and .succ is anonymous constructor notation: because Lean already knows the result must be a MyNat, we may write .zero in place of the full MyNat.zero. Writing plain zero would fail -- there is no zero in scope, only MyNat.zero.
Pattern matching works for any inductive type:
defnegate:BoolβBool|true=>false|false=>true
-- A function on Option Ξ±: extract or use a default
defOption.getD'{Ξ±:Type*}(d:Ξ±):OptionΞ±βΞ±|none=>d|somea=>a
-- A recursive function on List Ξ±
deflength'{Ξ±:Type*}:ListΞ±ββ|[]=>0|_::xs=>1+length'xs
Lean only accepts a recursive definition once it is convinced the
recursion terminates. For structural recursion -- where each
call is on a syntactic sub-part of the input, as in factorial,
length', and fib -- this is automatic.
When the recursive call is not on a structural sub-part, you
justify termination by giving a measure that strictly decreases:
the termination_by clause names the measure, and decreasing_by
discharges the proof that it goes down. Euclid's algorithm is the
classic example -- euclidGcd m n recurses on n % m, which is not
a subterm of m, but the first argument strictly decreases:
Reading it off: termination_by m declares the measure (the first
argument); for the recursive call Lean then asks you to show it
shrinks, i.e. n % m < m, which is exactly the goal decreasing_by
proves (Nat.mod_lt from m β 0; the hypothesis _h is in scope
thanks to the dependent if). When the decrease is routine,
decreasing_by can often close it with omega:
Under the hood this compiles to well-founded recursion
(WellFounded.fix); that theory -- what makes a relation
well-founded, and why < on Nat qualifies -- is developed in the
Mathematics part.
Typeclasses are one of the most important features of Lean, and they are central to how Mathlib organizes mathematics. A typeclass is a way to associate operations and properties with types, and to have Lean find the right implementation automatically. If you have seen abstract algebra, typeclasses are the mechanism that lets Lean know that the integers form a ring, that the real numbers form a field, and so on.
Consider the + operator. It works on natural numbers, integers, real numbers, matrices, polynomials, and many other types. How does Lean know which + to use?
One approach would be to define different functions: addNat, addInt, addReal, etc. But that would be extremely tedious. Instead, Lean uses typeclasses: there is a single Add typeclass, and each type that supports addition provides an instance of Add.
When you write a + b, Lean looks at the types of a and b, finds the appropriate Add instance, and uses it. This process is called instance resolution and happens automatically.
A typeclass is defined using the class keyword. Here is a simplified version of how Add might be defined:
classMyAdd(Ξ±:Type)wheremyAdd:Ξ±βΞ±βΞ±
This says: for any type Ξ±, an instance of MyAdd Ξ± provides a function myAdd : Ξ± β Ξ± β Ξ±. The class keyword is almost the same as structure -- in fact, every class is internally a structure. The difference is operational:
A class is also a structure, but Lean's instance resolution will synthesize one for you whenever a function expects an [Add Ξ±] argument. You never write the instance argument by hand.
So the rule of thumb is: use structure when the value is part of the data the user manipulates (a Point, a Person, a RingHom); use class when the value is a canonical piece of structure attached to a type (a Group instance on β€, an Add instance on β) and you want Lean to find it automatically.
Because a class is a structure, the applied class C Ξ± is an ordinary type, and an instance is simply a term of that type. Its universe follows the same rule as any structure -- the largest universe among its fields -- and this splits typeclasses into two kinds.
Data classes bundle operations, so they carry Type-valued fields and live in Type. Their instances are data: a record of operations and proofs, carried at runtime and projected like any structure.
Monoidβ : Type#check(Monoidβ) -- Type
inferInstance : Monoidβ#check(inferInstance:Monoidβ) -- a term of it -- data
Prop classes have only propositional fields, so they live in Prop, and their instances are proofs. Mathlib's Fact is the standard example -- it merely packages a proof of p as a searchable instance.
Fact : PropβProp#check@Fact -- Prop β Prop
Being a proposition, a Prop class inherits proof irrelevance: any two instances are definitionally equal. That is a feature -- such an instance can never conflict with another, and it is erased at runtime.
example(hβhβ:Fact(2=2)):hβ=hβ:=rfl
So typeclasses simply inherit the proof / data split we drew for terms in general: an instance is data or a proof exactly according to whether its class lives in Type or Prop.
It is worth being precise about the role such a term plays. You genuinely do construct it -- that is exactly what instance β¦ where β¦ does, and its fields are ordinary values you can project and run. But you then use it as a property of the type: declared once, synthesized automatically wherever [C Ξ±] is required, and essentially never named or passed by hand. For a data class it stays genuine data (carried at runtime); for a Prop class it degenerates to just the fact that it holds -- a proof, erased. So "an instance is only a property, not a term" is right about the usage but not the substance: the term is really there; you simply delegate its construction and handling to instance resolution.
The parallel is exact at the term level, too. Just as class is a structure marked for instance search, instance is a def marked for instance search: instance foo : C := β¦ is sugar for @[instance] def foo : C := β¦. (A plain def of a class type is a perfectly good term -- only invisible to the search.)
Writing instances by hand is not always necessary: for a range of
standard typeclasses Lean can generate the instance automatically, if
you append a deriving clause to the type definition.
structureStudentwherename:Stringage:βderivingReprdefalice:Student:={name:="Alice",age:=22}{ name := "Alice", age := 22 }#evalalice -- outputs { name := "Alice", age := 22 }
Without deriving Repr, the #eval command would not know how to print a Student. The deriving clause tells Lean to automatically create a Repr instance. Here is what the commonly derived classes provide:
Typeclass
What it provides
Repr
a way to display a value (needed by #eval)
BEq
Boolean equality a == b
Hashable
a hash function (for use in hash maps/sets)
Inhabited
a designated default value default : Ξ±
DecidableEq
a decidable equality test a = b (returns a proof either way)
The [Add Ξ±] argument tells Lean: "find an Add instance for Ξ± automatically." We do not need to pass it explicitly; Lean's instance resolution handles it.
You can name the instance if you need to refer to it:
But usually the unnamed form [Add Ξ±] is preferred, since Lean resolves it behind the scenes.
So [...] is the typeclass bracket: it marks an instance argument, filled by instance search -- as opposed to (...), which you supply yourself, and {...}, which Lean infers by unification. The four kinds of binder brackets (), {}, β¦β¦, and [] are contrasted side by side in the appendix on parentheses.
When working with Mathlib, it is often useful to see all the implicit and typeclass arguments of a lemma. The @ symbol makes all arguments explicit:
#check mul_comm
-- mul_comm : β {M : Type u_1} [inst : CommMonoid M] (a b : M), a * b = b * a
#check @mul_comm
-- @mul_comm : β {M : Type u_1} β CommMonoid M β M β M β M = M
The first form shows [inst : CommMonoid M] in brackets, meaning it is found automatically. The @ version shows all arguments explicitly, which helps understand exactly what typeclass constraints are required.
Mathlib uses typeclasses to build an extensive hierarchy of algebraic structures. Here are some key ones, ordered roughly from weakest to strongest:
Add Ξ± -- a type with an addition operation
Mul Ξ± -- a type with a multiplication operation
AddMonoid Ξ± -- a type with addition, an additive identity 0, and associativity
Monoid Ξ± -- a type with multiplication, a multiplicative identity 1, and associativity
Group Ξ± -- a monoid with inverses
AddCommGroup Ξ± -- a commutative group under addition
Ring Ξ± -- an additive commutative group with a multiplication that distributes over addition
CommRing Ξ± -- a ring with commutative multiplication
Field Ξ± -- a commutative ring where every nonzero element has an inverse
LinearOrder Ξ± -- a type with a total order
Each of these is defined as a class that extends simpler classes. For example (simplified):
class Group (Ξ± : Type) extends Monoid Ξ± where
inv : Ξ± β Ξ±
inv_mul_cancel : β a : Ξ±, inv a * a = 1
When you write a lemma that requires a CommRing, Lean automatically knows that it also has Add, Mul, AddCommGroup, etc., because of the extends chain.
The power of this system is that we can state and prove theorems at the most general level. For instance, mul_comm works for any CommMonoid, which includes β, β€, β, β, β, polynomial rings, matrix rings (when the base is commutative), and many more.
When Lean encounters an expression like a + b where a b : Ξ±, it needs to find an instance of Add Ξ±. The resolution proceeds as follows:
Lean looks through all registered instances (those declared with instance).
It tries to unify the goal Add Ξ± with the type of each instance.
If an instance itself requires other instances (e.g., Add (Prod Ξ± Ξ²) might require Add Ξ± and Add Ξ²), Lean recursively resolves those.
If exactly one chain of instances leads to a solution, Lean uses it. If none or multiple exist, it reports an error.
This process is deterministic and happens at elaboration time (when Lean checks your code), not at runtime. So there is no performance penalty.
You can trigger instance resolution yourself with the term
inferInstance, which asks Lean to synthesize an instance of a stated
type -- a handy way to ask "does this type have that structure?":
If no such instance exists, the command fails with a readable error
message. This is exactly the mechanism the [...] arguments rely on
behind the scenes.
By default, instance search treats all of a class's arguments as search keys: it must know them to look up an instance. Occasionally one argument is determined by the instance rather than given in advance. Marking it outParam tells Lean: do not wait for this argument -- find the instance from the others, and read this one off the result.
The standard example is heterogeneous multiplication a * b, whose result type need not match the arguments:
HMul : Type u_1βType u_2βoutParam(Type u_3)βType (max (max u_1 u_2) u_3)#check@HMul
-- HMul : Type β Type β outParam Type β Type
Because the result Ξ³ is an outParam, Lean resolves HMul Ξ± Ξ² ?Ξ³ from Ξ± and Ξ² alone and learnsΞ³ from the chosen instance. The same device lets a β s infer the element type from the collection -- Membership's element type is an outParam too. This is why you often see it in Mathlib signatures, and why the CoeSort β¦ β outParam Sort from the previous section is written that way.
One typeclass deserves special mention, because it sits at the border
between programs and proofs: Decidable. A proposition P is
Decidable when there is an algorithm that determines whether P
holds -- so the class carries genuine computational content, and lives
in Type, not Prop:
Decidable : PropβType#check@Decidable
-- Decidable : Prop β Type
-- Many concrete propositions are decidable, and the
-- instance is found by typeclass resolution:
inferInstance : Decidable(2+3=5)#check(inferInstance:Decidable(2+3=5)) -- isTrue ..
inferInstance : Decidable(2+3=6)#check(inferInstance:Decidable(2+3=6)) -- isFalse ..
Two everyday features rest on this instance search:
An if P then _ else _ requires a Decidable P instance -- that is
how the program knows which branch to take. This is also why
deriving DecidableEq (above) is useful: it makes
a = b decidable, so you may branch on equality of your own type.
The decide tactic closes any goal whose proposition is decidable,
simply by running that algorithm and checking the answer is
isTrue:
Where do those instances come from? A Decidable P instance is just a term returning one of the class's two constructors -- isTrue h carrying a proof h : P, or isFalse h carrying a proof h : Β¬P. You can write one by hand. Here is decidability of "is n zero?", by cases on n:
defdecZero(n:Nat):Decidable(n=0):=matchnwith|0=>isTruerfl -- a proof that 0 = 0
|m+1=>isFalse(Nat.succ_ne_zerom) -- a proof that m + 1 β 0
Each branch supplies a genuine proof -- that is what makes Decidable live in Type and carry content. Registered as an instance, decZero is exactly what decide runs and what if n = 0 then β¦ else β¦ consults; deriving DecidableEq just generates such a definition for you automatically.
Decidability is the constructive half of the story: it works without
any axioms, which is exactly why the resulting code is executable. The
classical counterpart -- making every proposition decidable
non-constructively, at the cost of executability -- belongs to the
axioms of Lean and is discussed in the Mathematics part.
A coercion is a conversion that Lean inserts automatically when a term of one type appears where another is expected -- for instance a β used where a β€ is wanted. Like everything in this chapter, it is driven by instance resolution: coercions are registered as instances of the class Coe, and when a mismatch could be bridged by one, Lean finds it and inserts the conversion, written β.
example(n:β):β€:=n -- inserted silently
example(n:β):β€:=βn -- the same, written explicitly
There are three flavours, according to what the target is:
Class
Converts a term into ...
Example
Coe
... a term of another type
β β β€; a subtype into its base type
CoeSort
... a type (where a Sort is expected)
a set as a type; Bool β Prop
CoeFun
... a function (in applied position)
a bundled morphism f applied as f x
Coe is the ordinary case -- one value becomes another:
example(s:{n:β//n>0}):β:=s -- the subtype's `.val`
CoeSort lets a term stand in for a type. This is exactly the Bool β Prop bridge from the previous section (b becomes b = true), and it is how a Set Ξ± may be written where a type is expected:
example(b:Bool):Prop:=bexample(s:Setβ):Type:=s -- the subtype β₯s of its members
CoeFun lets a bundled object be applied like a plain function -- so an algebra homomorphism can be written f x even though f is really a structure carrying that function together with its properties:
example(f:ββ+β)(n:β):β:=fn
Coercions keep statements readable: you write n + r for n : β and r : β, and Lean silently reads it as βn + r. The flip side is that a goal can quietly fill with βs that block tactics like ring or linarith; the appendix covers the numeric chain β β β€ β β β β and the norm_cast / push_cast tactics that tidy them up.
So (2 : Ξ±) means "the element of Ξ± denoted by the numeral 2", available for any Ξ± that has an OfNat Ξ± 2 instance. That is why the same 2 works across types,
2 : β€#check(2:β€)2 : β#check(2:β)
and why you can give a numeral to your own type simply by providing the instance:
A few further knobs control which instance is found. You rarely need them at first, but they show up when reading Mathlib:
Priorities.instance (priority := 1000) : β¦ makes an instance tried earlier (the default priority is 1000). Since Lean takes the first solution it finds, priorities resolve overlaps.
local and scoped instances. A local instance is active only until the end of the current section, namespace, or file; a scoped instance is active only inside its namespace or once you open it. Mathlib uses scoped heavily -- open scoped Classical, for instance, switches on classical decidability exactly where you want it.
Default instances.@[default_instance] supplies a fallback when synthesis would otherwise be ambiguous -- it is how a bare numeral like 2 defaults to β when nothing else pins down its type.
When you get an error like "failed to synthesize instance," it means Lean cannot find a required typeclass instance. Check that your type has the necessary instance defined.
Use #check @lemma_name to see all implicit arguments and understand what instances a lemma requires.
In Mathlib, most standard types (β, β€, β, β, β) have instances for all the common typeclasses. You rarely need to define instances for these.
When defining your own types, provide instances for the typeclasses you need. Start with Repr (for printing), then Add, Mul, etc., as needed.
The deriving mechanism can automatically generate instances for some typeclasses (like Repr, BEq, Hashable, Inhabited).
The Curry-Howard correspondence is one of the most profound ideas in the foundations of mathematics and computer science. It establishes a deep connection between logic and type theory: propositions correspond to types, and proofs correspond to terms (programs) of those types. In Lean 4, this correspondence is not merely a theoretical curiosity -- it is the very foundation on which the proof assistant is built.
The correspondence is named after Haskell Curry and William Alvin Howard, but its roots go deeper:
Haskell Curry (1934) observed that the types of combinators in combinatory logic correspond to axiom schemes in propositional logic.
William Howard (1969, published 1980) extended this to a full correspondence between natural deduction and simply typed lambda calculus.
Nicolaas Govert de Bruijn independently discovered the correspondence while developing the Automath system (1968), one of the first proof checkers.
Per Martin-LΓΆf (1971 onwards) developed intuitionistic type theory, extending the correspondence to predicate logic with dependent types.
Lean's type theory is a descendant of Martin-LΓΆf's work, enriched with features from the Calculus of Inductive Constructions (as used in Coq).
1.5.2.Β Propositions as types, proofs as termsπ
In Lean, every proposition P : Prop is a type. A proof of P is a term h : P -- that is, an inhabitant of the type P. If a type is inhabited, the corresponding proposition is true; if it is empty, the proposition is false.
This idea is sometimes called the BHK interpretation (Brouwer-Heyting-Kolmogorov), which gives a constructive meaning to the logical connectives. Let us see how each connective maps to a type-theoretic construction.
1.5.3.Β Tactic mode, term mode, and the proof stateπ
Before we go through the connectives, we need to know how to write a proof, since every example below does. Because a proof is just a term of the proposition's type, there are two ways to build it. In term mode you write the term directly; in tactic mode (after the keyword by) you build it step by step with tactics. With the cursor inside a tactic block, Lean shows the proof state: the hypotheses appear above the β’ (turnstile) and the goal to its right. The sorry tactic closes any goal as a placeholder (with a warning), so you can leave holes while developing a proof.
The two modes are two views of the same thing. Two rules of thumb, which the connectives below make precise:
the tactic exact is the same as calling a function;
the tactic intro is like taking a variable that becomes the argument of a function.
The practical workflow -- and which tactics to reach for -- is taken up in the last section of this chapter; here we use by, intro, and exact just enough to exhibit the correspondence.
1.5.4.Β True and False: the unit and empty typesπ
The correspondence is clearest at the two extremes. A proposition that is always provable should be a type that is always inhabited; a proposition that can never be proved should be a type with no inhabitants at all. Lean's True and False are exactly these two types -- both ordinary inductive types, introduced in the chapter on the natural numbers (True with a single constructor, False with none). Here we only look at them through the Curry-Howard lens.
True has the canonical proof True.intro, so it is inhabited and proving it is trivial:
False has no constructor, so no proof of it can ever be built; and its recursor, having no cases, hands us any goal from a proof of False -- the principle ex falso quodlibet (packaged as False.elim):
The most fundamental instance of the correspondence: implication corresponds to the function type.
If P and Q are propositions, then P β Q is the type of functions from P to Q. A proof of P β Q is a function that, given a proof of P, produces a proof of Q.
Here is the same theorem proved in two ways -- by tactic and by term:
example(PQ:Prop)(hP:P)(hPQ:PβQ):Q:=P:PropQ:ProphP:PhPQ:PβQβ’ QAll goals completed! π
-- Term proof: just function application!
example(PQ:Prop)(hP:P)(hPQ:PβQ):Q:=hPQhP
A proof of P β Q β R is a function of two arguments (curried):
example(PQR:Prop):(PβQ)β(QβR)β(PβR):=P:PropQ:PropR:Propβ’ (PβQ)β(QβR)βPβRintrohPQP:PropQ:PropR:ProphPQ:PβQhQR:QβRβ’ PβRP:PropQ:PropR:ProphPQ:PβQhQR:QβRhP:Pβ’ RAll goals completed! π
-- Term proof: composition of functions
example(PQR:Prop):(PβQ)β(QβR)β(PβR):=funhPQhQRhP=>hQR(hPQhP)
The disjunction P β¨ Q corresponds to the sum type (coproduct). A proof of P β¨ Q is either a proof of P (tagged with Or.inl) or a proof of Q (tagged with Or.inr).
example(PQ:Prop)(hP:P):Pβ¨Q:=P:PropQ:ProphP:Pβ’ Pβ¨QP:PropQ:ProphP:Pβ’ PAll goals completed! π
-- Term proof: inject into the left summand
example(PQ:Prop)(hP:P):Pβ¨Q:=Or.inlhP
-- Eliminating a disjunction: case analysis
example(PQR:Prop)(h:Pβ¨Q)(hPR:PβR)(hQR:QβR):R:=h.elimhPRhQR
Negation Β¬P is defined as P β False. That is, a proof of Β¬P is a function that takes a hypothetical proof of P and derives a contradiction.
example(P:Prop):(Β¬P)=(PβFalse):=rfl
-- Tactic proof
example(P:Prop)(hP:P)(hnP:Β¬P):False:=P:ProphP:PhnP:Β¬Pβ’ FalseAll goals completed! π
-- Term proof: function application
example(P:Prop)(hP:P)(hnP:Β¬P):False:=hnPhP
-- Ex falso: from False, anything follows
example(P:Prop)(h:False):P:=h.elim
1.5.9.Β Universal quantifier = dependent function type (Pi type)π
The universal quantifier β (x : Ξ±), P x corresponds to the dependent function type (Pi type) (x : Ξ±) β P x. A proof is a function that, for each x : Ξ±, produces a proof of P x.
example:β(n:β),n=n:=β’ β(n:β),n=nn:ββ’ n=nAll goals completed! π
-- Term proof: a dependent function (lambda)
example:β(n:β),n=n:=funn=>rfl
Notice that in Lean, β and the dependent arrow β are actually the same thing. When the codomain does not depend on the input, the dependent function type degenerates to the ordinary function type.
1.5.10.Β Existential quantifier = dependent pair type (Sigma type)π
The existential quantifier β (x : Ξ±), P x corresponds to the dependent pair type. A proof consists of a witness a : Ξ± together with a proof that P a holds.
Note: There is an important distinction between β (which lives in Prop) and Ξ£ (which lives in Type). We discuss this further in the difference between β and Ξ£.
1.5.11.Β Nonempty and propositional truncationπ
The Curry-Howard view says "to prove β x, P x, give an x and a
proof of P x". But sometimes you want to express that some
element of a type Ξ± exists, without committing to a specific
witness. For this Lean has the typeclass
Nonempty : Sort u_1βProp#check@Nonempty
Nonempty Ξ± is a proposition: it asserts only that Ξ± has at
least one element, but it does not give you one. This is the
propositional truncation of Ξ±: it forgets the witness, keeping
only the bare existence claim.
To extract data from a Nonempty (i.e., to actually obtain a term
of Ξ±), you must use Classical.choice, an axiom of Lean's type
theory:
This is a real axiom: constructively, knowing only that Ξ± is
nonempty does not let you pick an element. Classical.choice
turns the propositional fact into honest data, at the cost of being
nonconstructive.
This pair (Nonempty.intro, Classical.choice) is the prototypical
example of moving data between Type and Prop. In Mathlib it is
the foundation for many "exists, therefore choose one" arguments,
and the reason noncomputable appears so often.
A related construction is Squash Ξ±, the explicit propositional
truncation of Ξ± -- it has a single constructor Squash.mk a for
any a : Ξ±, and sits at the boundary between Prop and Type.
1.5.12.Β How tactic proofs build terms behind the scenesπ
When you write a tactic proof in Lean, the tactic block constructs a term behind the scenes. Every tactic manipulates the proof state and ultimately produces a term of the goal type. You can use show_term (or look at the output of #print) to see what term a tactic proof generates.
Understanding this correspondence is key to becoming fluent in Lean: when you write a tactic proof, you are really constructing a term; when you write a term proof, you are programming a function. The two perspectives are equivalent, and switching between them often leads to deeper understanding.
The correspondence is not only elegant, it is how you actually prove
things: to prove a statement you construct a term of its type. Most
statements you meet are far beyond a one-liner -- Goldbach's
conjecture, for instance, is a perfectly well-formed term of type
Prop:
A term of this type -- something that would replace the sorry -- is
a proof of the conjecture. Constructing a term of type β is easy
(0 : β will do); constructing this one would require actually
proving Goldbach.
The core tactics you meet first are intro, exact, apply, rw,
have, obtain, refine, and use, together with the
library-search helpers apply? and simp?. Rather than tabulate
them here, we introduce each one hands-on in the exercises, right
where you first need it -- so you can experiment immediately. The
complete alphabetical reference, with many more tactics, lives in the
Tactics appendix, and searching Mathlib for the
right lemma is covered in Navigating
Mathlib.
It is now time to move to the exercises. Proceed to vscode (or
gitpod), copy the exercises folder, and start coding. Each sheet
introduces the tactics it needs; the Tactics
appendix gives the alphabetical reference.
Dependent types are the feature that distinguishes Lean's type system from those of most mainstream programming languages. In a dependently typed language, types can depend on values. This single idea has far-reaching consequences: it allows us to express precise specifications, encode mathematical statements, and catch entire classes of errors at compile time.
In a simply typed language (like Haskell without extensions, or basic OCaml), the type of a function's output cannot depend on the value of its input -- only on the type of its input. For example, you can write f : β β β, but you cannot write a type that says "a list whose length equals the input n."
In a dependently typed language, you can do this. The type of the output may mention the input value. For example:
(n : β) β Fin n β β is the type of a function that takes a natural number n and then an element of Fin n, returning a real number.
(n : β) β Vector β n is the type of a function that, given n, returns a vector of n real numbers.
This expressiveness is what allows Lean to serve simultaneously as a programming language and a theorem prover.
The Pi type (also written Ξ -type) is the type of dependent functions. In Lean, we write it as:
(x : Ξ±) β Ξ² x
where Ξ² : Ξ± β Sort u is a type family indexed by Ξ±. For each value a : Ξ±, the function returns a term of type Ξ² a. When Ξ² does not actually depend on x, this reduces to the ordinary function type Ξ± β Ξ².
In Lean, β is notation for Pi types when the codomain is a Prop:
Another important example: a function that, given a type, returns the identity function on that type. This is a function whose return type depends on its input:
This is a subtle but important distinction in Lean:
Ξ£ (x : Ξ±), Ξ² x lives in Type -- you can extract the witness and use it computationally.
β (x : Ξ±), P x lives in Prop -- the witness is "erased" and cannot be used in computations (only in proofs).
This reflects the principle of proof irrelevance: in Prop, the specific proof does not matter, only whether the proposition is true or false. In Type, the data matters.
A classic example of dependent types is the type of vectors (lists of a fixed length). In Mathlib, one way to represent a vector of length n with entries in Ξ± is as a function Fin n β Ξ± (recall Fin n, the type with n elements, from the Types chapter).
The advantage of this representation is that indexing is always safe: you cannot access index 5 of a 3-element vector, because 5 is not a term of type Fin 3. The type system prevents out-of-bounds errors at compile time.
Matrices are a natural extension. An m Γ n matrix with entries in Ξ± can be represented as a function Fin m β Fin n β Ξ±, or equivalently as Matrix (Fin m) (Fin n) Ξ± in Mathlib:
In Mathlib, matrix multiplication is only defined when the dimensions match. If A is an m Γ n matrix and B is an n Γ p matrix, then A * B is an m Γ p matrix. The dependent types enforce that the inner dimensions agree -- a dimension mismatch is a type error, caught at compile time.
A type family is simply a function that returns a type. Type families are pervasive in Lean and Mathlib:
defBoolFamily:BoolβType|true=>β|false=>String
-- Dependent function using the type family
defboolFamilyExample:(b:Bool)βBoolFamilyb|true=>(42:β)|false=>("hello":String)
In mathematical formalization, type families appear everywhere. For instance, the fiber of a function f : Ξ± β Ξ² over a point b : Ξ² is the subtype {a : Ξ± // f a = b}, which is a type family indexed by Ξ².
The subtype{x : Ξ± // P x} is a special case of a Sigma type where P : Ξ± β Prop. It represents elements of Ξ± satisfying predicate P. Since the second component is a proof (in Prop), subtypes enjoy proof irrelevance: two elements of {x : Ξ± // P x} are equal if and only if their first components are equal.
This is how many mathematical objects are defined in Mathlib. For example, the type of prime numbers could be defined as {n : β // Nat.Prime n}.
A subtype is closely related to, but distinct from, a sets : Set Ξ±: a set selects elements of Ξ± (which keep their type Ξ±), whereas the subtype {x : Ξ± // P x} is a new type. The relationship -- including the coercion β₯s from a set to its subtype of members -- is spelled out in the chapter on sets and types.
Dependent types make Lean strictly more expressive than simply-typed languages like Haskell or OCaml. Here are some things you can express with dependent types that you cannot express in simply-typed languages:
Length-indexed collections: The type system guarantees that head is never called on an empty list, that matrix dimensions match in multiplication, etc.
Precise specifications: You can write a sorting function whose type guarantees that the output is sorted and is a permutation of the input.
Mathematical theorems: The type β (n : β), β (p : β), p > n β§ Nat.Prime p expresses Euclid's theorem (infinitely many primes). A term of this type is a proof.
Safe APIs: You can define a division function (a : β€) β (b : β€) β b β 0 β β€ that requires a proof of non-zero denominator. No runtime exceptions possible.
defsafeDiv(ab:β€)(hb:bβ 0):β€:=a/b
-- This works:
3#evalsafeDiv103(β’ 3β 0All goals completed! π)
-- This would be a type error (uncomment to see):
-- #eval safeDiv 10 0 (by norm_num) -- norm_num cannot prove 0 β 0
The price of this expressiveness is that type checking becomes undecidable in general (though Lean uses various heuristics to make it practical). But for mathematics, the payoff is enormous: the type system itself becomes a logic in which we can state and prove theorems.
Lean allows you to define custom notation using the notation command. This is useful when you want a concise mathematical symbol for a frequently used expression. The general syntax is
notation "symbol" arg1 arg2 ... => expression
where the left-hand side describes the syntax pattern (with arguments) and the right-hand side is the Lean expression it expands to. Here is a simple example:
After this definition, Ξ΄ is replaced by 2 everywhere. The notation is purely syntactic: Lean replaces every occurrence of the new notation by the right-hand side before type checking. Here is a more interesting example with an argument:
Here, infixl means left-associative infix, 65 is the binding power (the same as +), and the spaces around ββ are part of the syntax. Similarly, infixr gives right-associative infix, and prefix / postfix are available for unary operators.
notation can take more than one argument and mix custom tokens with them:
sectionTernaryDemo
-- "between a and b" means the half-open interval [a, b)
notation"between "a" and "b=>Set.Icoabbetween1and10 : Setβ#checkbetween(1:β)and10 -- Set β
endTernaryDemo
Mathlib uses notation3 (and its underlying machinery syntax +
macro_rules) to introduce binder-style notation like
β k β range n, f k and βαΆ x in F, p x. These let you bind a
variable inside the expression that follows the comma. Writing such
notation from scratch involves a fair amount of macro plumbing and
is beyond the scope of this section -- the standard reference is the
Lean 4 documentation on macros.
For ordinary day-to-day notation, plain notation, infixl,
infixr, prefix, and postfix are almost always enough.
If a notation should only be active inside a namespace (so it does
not pollute the global symbol space), mark it scoped:
namespaceMyDemoscopednotation"β¦"=>(42:β)endMyDemo
-- Outside the namespace, `β¦` is not in scope by default;
-- enable it with `open MyDemo` or `open scoped MyDemo`.
openscopedMyDemo42#eval⦠-- 42
This is the pattern used throughout Mathlib for notation like π,
π, β«, βαΆ , etc.: they are scoped to the relevant namespace and
enabled with open scoped.
For more complex notation involving multiple tokens or bespoke
parsing rules, you can use the syntax and macro_rules commands,
but notation, the infix variants, prefix/postfix, and
notation3 cover most common use cases.
There are at least two abbreviations used in Mathlib which you will encounter frequently.
If you have h : x = y and hx : P x (with P x : Prop), you can prove P y by replacing h in hx. The shorthand notation for this is h βΈ hx. (Write \t for βΈ).
Sometimes, bracketing is critical, and it appears frequently that it has the form
apply first (second very long statement), and you might get lost since the closing brackets are far away from their opening counterparts. In this case, we write apply first <| second very long statement, which does not need a closing symbol.
The abbrev command introduces a definition that is reducibly transparent, meaning Lean's elaborator will unfold it automatically whenever needed. In contrast, a definition introduced with def is semireducible and will not be unfolded automatically.
abbrevNatAlias:=β
After this, NatAlias and β are interchangeable everywhere β Lean treats them as definitionally equal without needing any extra work. In particular, all type class instances that apply to β are automatically available for NatAlias. Compare this with
defNatAlias':=β
where NatAlias' is a genuinely new type: Lean will not automatically apply β instances to NatAlias', and you would have to derive or register them yourself.
Use abbrev when you want a short name for a type or expression but do not want to create a new abstraction barrier. Use def when you intentionally want to hide the definition (e.g. to prevent the simplifier from unfolding it).