Interactive Theorem Proving using Lean, Winter 2026/27

1.Β Lean and its type theoryπŸ”—

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.)

1.1.Β IntroductionπŸ”—

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.

1.1.1.Β The natural numbersπŸ”—

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 recursor Nat.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):

example (C : Type) (base : C) (step : Nat β†’ C β†’ C) : Nat β†’ C := fun n ↦ Nat.rec base step n

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.rec computes. 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):

def step (_ j : Nat) : Nat := j + 2 def myDouble (n : Nat) : Nat := Nat.rec 0 step n -- myDouble 2 -- ⟢ Nat.rec 0 step 2 -- δ: unfold myDouble -- ⟢ step 1 (Nat.rec 0 step 1) -- ι on succ, then δβ -- ⟢ (Nat.rec 0 step 1) + 2 -- δ: unfold step -- ⟢ (step 0 (Nat.rec 0 step 0)) + 2 -- ι again -- ⟢ ((Nat.rec 0 step 0) + 2) + 2 -- δ: unfold step -- ⟢ (0 + 2) + 2 ⟢ 4 -- ι on zero, then + example : myDouble 2 = 4 := rfl

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.

1.1.2.Β Definitional equality and rflπŸ”—

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 equality a = 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 := rfl example : Nat.succ 0 = 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".

The reduction rulesπŸ”—

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 : (fun x => x + 1) 4 = 5 := rfl example : 2 + 2 = 4 := rfl example : List.length [7, 8, 9] = 3 := rfl -- eta: a function equals its eta-expansion example (f : Nat β†’ Nat) : (fun x => f x) = f := rfl -- zeta: a `let` unfolds example : (let x := 4; x + x) = 8 := rfl -- delta + iota: a defined function on a literal def twice (n : Nat) : Nat := 2 * n example : twice 3 = 6 := rfl

Conversely, rfl fails 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 mismatch rfl has type ?m.9 = ?m.9 but is expected to have type 0 + 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 = n induction n with ⊒ 0 + 0 = 0 All 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:

example (r : Nat β†’ Nat β†’ Prop) (f : Nat β†’ Nat) (h : βˆ€ a b, r a b β†’ f a = f b) (a : Nat) : Quot.lift f h (Quot.mk r a) = f a := rfl
Ξ· for structures

The Ξ· rule also applies to structures: a structure value equals the record of its own projections, s ≑ ⟨s.1, s.2, β€¦βŸ© (Ξ· for structures). A pair of naturals, for instance, is definitionally its own pair of projections:

example (p : Nat Γ— Nat) : p = (p.1, p.2) := rfl
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 and the elaboratorπŸ”—

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.

1.1.3.Β BoolπŸ”—

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):

πŸ”—inductive type
Bool : Type
Bool : Type

The Boolean values, true and false.

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.

Constructors

false : Bool

The Boolean value false, not to be confused with the proposition False.

true : Bool

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 : (bif true then "Hi" else "Salut") = "Hi" := rfl -- and in general `bif b then f else e` is `Bool.rec e f b`: example {Ξ± : Type} (e f : Ξ±) : (bif false then f else e) = Bool.rec e f false := 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) : (bif b then "Hi" else "Salut") = Bool.rec "Salut" "Hi" b := b:Bool⊒ (bif b then "Hi" else "Salut") = Bool.rec "Salut" "Hi" b Tactic `rfl` failed: The left-hand side bif b then "Hi" else "Salut" is not definitionally equal to the right-hand side Bool.rec "Salut" "Hi" b b:Bool⊒ (bif b then "Hi" else "Salut") = Bool.rec "Salut" "Hi" bb:Bool⊒ (bif b then "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):

example (b : Bool) : (bif b then "Hi" else "Salut") = Bool.rec "Salut" "Hi" b := b:Bool⊒ (bif b then "Hi" else "Salut") = Bool.rec "Salut" "Hi" b ⊒ (bif false then "Hi" else "Salut") = Bool.rec "Salut" "Hi" false⊒ (bif true then "Hi" else "Salut") = Bool.rec "Salut" "Hi" true ⊒ (bif false then "Hi" else "Salut") = Bool.rec "Salut" "Hi" false⊒ (bif true then "Hi" else "Salut") = Bool.rec "Salut" "Hi" true All goals completed! πŸ™

Note that Bool also carries the usual &&, ||, !.

How bif is defined under the hood

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)

1.1.4.Β The propositions True and FalseπŸ”—

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):

πŸ”—inductive proposition
True : Prop
True : Prop

True is a proposition and has only an introduction rule, True.intro : True. In other words, True is simply true, and has a canonical proof, True.intro For more information: Propositional Logic

Constructors

intro : True

True is true, and True.intro (or more commonly, trivial) is the proof.

So proving True is as easy as naming that constructor:

example : True := True.intro

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:

πŸ”—inductive proposition
False : Prop
False : Prop

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):

False.rec : (motive : False β†’ Sort u) β†’ (t : False) β†’ motive t
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
example (h : False) : 0 = 1 := False.elim h example (h : False) (P : Prop) : P := h.elim

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.

1.2.Β TypesπŸ”—

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.

1.2.1.Β The universe hierarchyπŸ”—

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 : Type 1) -- Type : Type 1 Type 1 : Type 2#check (Type 1 : Type 2) -- Type 1 : Type 2
The connection of Type to Sort

The whole tower is captured by a single keyword Sort:

  • Sort 0 is Prop;

  • Sort 1 is Type 0, i.e. Type;

  • Sort (u+1) is Type u.

example : Sort 0 = Prop := rfl example : Sort 1 = Type := rfl example : Type = Type 0 := rfl

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 (βˆ€ Ξ± : Type 5, Ξ± β†’ Ξ±) -- Type 6

This climbing is what it means for the Type u to be predicative: a predicative definition refers to things already available below it -- whatever a type quantifies over must be strictly smaller, so the type it forms can never land back inside its own domain. That is exactly why βˆ€ Ξ± : Type 5, Ξ± β†’ Ξ± is pushed up to Type 6, safely above everything it ranges over, and it is what keeps the whole hierarchy well-founded. (The terminology goes back to PoincarΓ© and Russell, who imposed predicativity to outlaw the self-referential definitions behind the set-theoretic paradoxes.)

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.

1.2.3.Β Why Prop is specialπŸ”—

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 (a b : β„•) : a = b := Type mismatch rfl has type ?m.3 = ?m.3 but is expected to have type a = brfl

See also Prop vs Type.

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:

βˆ€ (Ξ± : Type), Ξ± = Ξ± : Prop#check (βˆ€ Ξ± : Type, Ξ± = Ξ±) -- Prop βˆ€ (Ξ± : Type 5), Ξ± = Ξ± : Prop#check (βˆ€ Ξ± : Type 5, Ξ± = Ξ±) -- Prop

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 proposition P : 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 (a b : 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⊒ motive h✝ 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.nilmatch h with | 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:

example {a : Prop} (h : a) : (Or.inl (b := a) h) = Or.inr (b := a) h := rfl

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 every a ∨ 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 : βˆ€ {a b : Prop}, a ∨ b β†’ Bool) (left : βˆ€ {a b : Prop} (h : a), which (Or.inl h : a ∨ b) = true) (right : βˆ€ {a b : Prop} (h : a), which (Or.inr h : b ∨ a) = false) : False := which:{a b : Prop} β†’ a ∨ b β†’ Boolleft:βˆ€ {a b : Prop} (h : a), which β‹― = trueright:βˆ€ {a b : Prop} (h : a), which β‹― = false⊒ False -- at `a, b := True`, the injections collide: have htf : true = false := which:{a b : Prop} β†’ a ∨ b β†’ Boolleft:βˆ€ {a b : Prop} (h : a), which β‹― = trueright:βˆ€ {a b : Prop} (h : a), which β‹― = false⊒ False All goals completed! πŸ™ which:{a b : Prop} β†’ a ∨ b β†’ Boolleft:βˆ€ {a b : Prop} (h : a), which β‹― = trueright:βˆ€ {a b : Prop} (h : a), which β‹― = false⊒ true = false β†’ False All 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):

πŸ”—inductive type
Decidable (p : Prop) : Type
Decidable (p : Prop) : Type

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 Decidable p, attempts to reduce it to isTrue h, 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 {_ : Decidable p} rather than [Decidable p] so that non-canonical instances can be found via unification rather than instance synthesis.

Constructors

isFalse {p : Prop} (h : Β¬p) : Decidable p

Proves that p is decidable by supplying a proof of Β¬p

isTrue {p : Prop} (h : p) : Decidable p

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 dependent if, whose h : p names the proof made available in the then-branch) and the decide tactic compute. The two stories line up exactly:

Proposition (in Prop)

Data version (in Type)

what stays hidden

βˆƒ x, q x

Ξ£ x, q x

the witness

a ∨ ¬a

Decidable a

the tag (which side)

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.)

1.2.4.Β Inductive typesπŸ”—

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:

inductive MyNat where | zero : MyNat | succ (n : MyNat) : MyNat

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 recursor MyNat.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 motive MyNat β†’ 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} β†’ motive MyNat.zero β†’ ((n : MyNat) β†’ motive n β†’ motive n.succ) β†’ (t : MyNat) β†’ motive t#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 also cover non-recursive data:

inductive Colour where | red | green | blue

and parameterized types:

inductive MyOption (Ξ± : Type) where | none : MyOption Ξ± | some (a : Ξ±) : MyOption Ξ±

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 β†’:

inductive MyTree where | leaf : β„• β†’ MyTree -- a leaf holds a value | node : (β„• β†’ MyTree) β†’ MyTree -- positive occurrence -- leaves now carry data, and trees can be built: example : MyTree := .leaf 7 example : MyTree := .node (fun n => .leaf n)

The leaf case is what gets the type off the ground, and it is worth pausing on why it is needed. Positivity constrains only where MyTree 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 every MyTree 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

so neg : Bad β†’ False. Then Bad.mk neg : Bad, and neg (Bad.mk neg) : False is a closed proof of False (it even loops, neg (Bad.mk neg) ⟢ neg (Bad.mk neg) -- the untyped Ξ© combinator smuggled into the theory). Strict positivity outlaws exactly the negative occurrence that ties this self-applying knot -- the same consistency concern that rules out Type : Type. (That positivity is also sufficient for consistency -- every strictly positive type is the least fixed point of a monotone functor, so the theory has a model -- is a deep metatheorem, not something Lean proves about itself.)

1.2.5.Β StructuresπŸ”—

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:

structure Point where x : β„• 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:

Point.mk : β„• β†’ β„• β†’ Point#check (Point.mk : β„• β†’ β„• β†’ Point) Point.x : Point β†’ β„•#check (Point.x : Point β†’ β„•) Point.y : Point β†’ β„•#check (Point.y : Point β†’ β„•)

Fields may be given default values as part of the declaration:

structure MyConfig where width : β„• := 80 height : β„• := 24 title : String := "Untitled"

These defaults are used whenever a value is built without specifying every field.

One structure can extend another, inheriting all of its fields:

structure Point3D extends Point where z : β„•

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,

structure MyComplex where re : β„€ 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):

structure MyLinearMap (Ξ± Ξ² : Type) [Add Ξ±] [Add Ξ²] where toFun : Ξ± β†’ Ξ² map_add : βˆ€ x y : Ξ±, toFun (x + y) = toFun x + toFun y

This pattern of bundling data with properties is fundamental to how Mathlib organizes mathematics.

How to construct values of these types, read their fields, and define operations on them is the subject of the next chapter.

1.2.6.Β Inductive types vs structuresπŸ”—

When should you use inductive and when structure?

  • 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.)

1.2.7.Β Quotient typesπŸ”—

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) β†’ Ξ± β†’ Quot r#check @Quot.mk -- (r : Ξ± β†’ Ξ± β†’ Prop) β†’ Ξ± β†’ Quot r @Quot.sound : βˆ€ {Ξ± : Sort u_1} {r : Ξ± β†’ Ξ± β†’ Prop} {a b : Ξ±}, r a b β†’ Quot.mk r a = Quot.mk r b#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 : Ξ± β†’ Ξ²) β†’ (βˆ€ (a b : Ξ±), r a b β†’ f a = f b) β†’ Quot r β†’ Ξ²#check @Quot.lift -- (f : Ξ± β†’ Ξ²) β†’ (βˆ€ a b, r a b β†’ f a = f b) β†’ Quot r β†’ Ξ² @Quot.ind : βˆ€ {Ξ± : Sort u_1} {r : Ξ± β†’ Ξ± β†’ Prop} {Ξ² : Quot r β†’ Prop}, (βˆ€ (a : Ξ±), Ξ² (Quot.mk r a)) β†’ βˆ€ (q : Quot r), Ξ² 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.Cauchy abs β†’ ℝ#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 : IsAbsoluteValue abv] β†’ CauSeq Ξ² abv β†’ CauSeq.Completion.Cauchy abv#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 (f g : 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:

example (f g : CauSeq β„š abs) : CauSeq.Completion.mk f + CauSeq.Completion.mk g = CauSeq.Completion.mk (f + g) := CauSeq.Completion.mk_add f g

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.Β TermsπŸ”—

The previous chapter showed how types are formed. This chapter is about the other side: how to define terms -- named inhabitants of those types.

1.3.1.Β Naming terms with defπŸ”—

The command def gives a name to a term. In its simplest form,

def name : T := term

gives the name name to a term of type T. Using the Point structure from the previous chapter, here are four ways to define a term of type Point:

def origin : Point := { x := 0, y := 0 } def p1 : Point := ⟨1, 2⟩ def p2 : Point := Point.mk 3 4 def p3 : Point where x := 0 y := 0

All four invoke the single constructor of Point: the record form { x := …, y := … }, the anonymous constructor ⟨...⟩ (typed \< and \>), the explicit Point.mk, and the block where form -- one field := value line per field, with no braces or commas. The where form is just the record form spread over several lines; it is the same syntax we will use to supply typeclass instances (instance : Add Vec2 where …), and it has nothing to do with the |-branches of pattern matching (fields are field := value, not | pattern => …).

Here the target type is fixed by the : Point on the left of :=, so Lean knows which constructor { … } and βŸ¨β€¦βŸ© mean. When the type is not clear from the context, you can annotate the record itself:

{ x := 0, y := 0 } : Point#check ({ x := 0, y := 0 } : Point)

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:

1#eval p1.x -- outputs 1 2#eval p1.y -- outputs 2

We build a new value from an existing one, changing only some fields, with the with keyword:

def p4 : Point := { p1 with y := 10 } -- p4.x = 1, p4.y = 10

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:

def myConfig : MyConfig := { title := "My Window" } -- myConfig.width = 80, myConfig.height = 24

For an extended structure we supply all fields, inherited and new:

def q : Point3D := { x := 1, y := 2, z := 3 } 1#eval q.x -- outputs 1 (inherited from Point) 3#eval q.z -- outputs 3

Operations on a structure are ordinary functions; placing them in the type's namespace lets us call them with dot notation:

def Point.normSq (p : Point) : β„• := p.x * p.x + p.y * p.y 25#eval p2.normSq -- outputs 25

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:

def MyComplex.add (a b : MyComplex) : MyComplex := { re := a.re + b.re, im := a.im + b.im } def MyComplex.mul (a b : MyComplex) : MyComplex := { re := a.re * b.re - a.im * b.im, im := a.re * b.im + a.im * b.re } def MyComplex.norm (a : MyComplex) : β„€ := a.re * a.re + a.im * a.im def i : MyComplex := { re := 0, im := 1 } -1#eval (MyComplex.mul i i).re -- outputs -1

1.3.3.Β Defining and evaluating functionsπŸ”—

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:

def double (n : β„•) : β„• := 2 * n 10#eval double 5 -- outputs 10 0#eval double 0 -- outputs 0

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.

def add (a b : β„•) : β„• := a + b 7#eval add 3 4 -- outputs 7

The type of add is β„• β†’ β„• β†’ β„•, which reads as β„• β†’ (β„• β†’ β„•); so add 3 is itself a function of type β„• β†’ β„•.

1.3.4.Β Polymorphic functionsπŸ”—

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:

def myId (Ξ± : Type) (a : Ξ±) : Ξ± := a myId β„• 42 : β„•#check myId β„• 42 -- β„•

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:

def myId' {Ξ± : Type} (a : Ξ±) : Ξ± := a myId' 42 : β„•#check myId' 42 -- β„•, inferred

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:

universe u def idOne (Ξ± : Type u) (a : Ξ±) : Ξ± := a def idStar {Ξ± : Type*} (a : Ξ±) : Ξ± := a

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.

1.3.5.Β Anonymous functionsπŸ”—

Sometimes we need a quick, throwaway function without giving it a name. We use fun (short for "function") with the ↦ arrow (typed \mapsto):

6#eval (fun x : β„• ↦ x + 1) 5 -- outputs 6 12#eval (fun x y : β„• ↦ x * y) 3 4 -- outputs 12

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.

1.3.6.Β Pattern matchingπŸ”—

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:

def factorial : β„• β†’ β„• | 0 => 1 | n + 1 => (n + 1) * factorial n

Each branch may use the variables introduced by its pattern (here n on the right-hand side). Lean checks two things automatically:

  1. Exhaustiveness. Every constructor of β„• is covered (the cases 0 and n + 1 exhaust β„•). If you forget a case, Lean complains.

  2. 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:

def MyNat.double : MyNat β†’ MyNat | .zero => .zero | .succ n => .succ (.succ (MyNat.double n))

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:

def negate : Bool β†’ Bool | true => false | false => true -- A function on Option Ξ±: extract or use a default def Option.getD' {Ξ± : Type*} (d : Ξ±) : Option Ξ± β†’ Ξ± | none => d | some a => a -- A recursive function on List Ξ± def length' {Ξ± : Type*} : List Ξ± β†’ β„• | [] => 0 | _ :: xs => 1 + length' xs

The same syntax can be used inline with match:

example (n : β„•) : β„• := match n with | 0 => 42 | _ + 1 => 0

1.3.7.Β Recursion and terminationπŸ”—

The definitions above are recursive: they call themselves on smaller inputs. A classic example with two base cases is the Fibonacci sequence:

def fib : β„• β†’ β„• | 0 => 0 | 1 => 1 | n + 2 => fib n + fib (n + 1) 55#eval fib 10 -- outputs 55

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:

def euclidGcd (m n : Nat) : Nat := if _h : m = 0 then n else euclidGcd (n % m) m termination_by m decreasing_by All goals completed! πŸ™ 12#eval euclidGcd 48 36 -- 12

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:

def log2 (n : Nat) : Nat := if _h : 2 ≀ n then 1 + log2 (n / 2) else 0 termination_by n decreasing_by All goals completed! πŸ™ 4#eval log2 16 -- 4

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.

1.4.Β TypeclassesπŸ”—

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.

1.4.1.Β The problem typeclasses solveπŸ”—

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.

1.4.2.Β Defining typeclassesπŸ”—

A typeclass is defined using the class keyword. Here is a simplified version of how Add might be defined:

class MyAdd (Ξ± : Type) where myAdd : Ξ± β†’ Ξ± β†’ Ξ±

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 structure is constructed and passed explicitly: you write ⟨...⟩ or call its named projections.

  • 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.

1.4.3.Β Data classes and Prop classesπŸ”—

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.)

Level

plain

registered for search

type (constructor)

structure

class

term (value)

def

instance

1.4.4.Β Creating instancesπŸ”—

We create instances using the instance keyword:

instance : MyAdd β„• where myAdd := Nat.add instance : MyAdd β„€ where myAdd := Int.add

Now we can use MyAdd.myAdd on natural numbers and integers, and Lean will automatically find the right instance.

Let us define a custom type and give it an Add instance:

structure Vec2 where x : Float y : Float instance : Add Vec2 where add a b := { x := a.x + b.x, y := a.y + b.y } instance : ToString Vec2 where toString v := s!"({v.x}, {v.y})" (4.000000, 6.000000)#eval (⟨1.0, 2.0⟩ : Vec2) + ⟨3.0, 4.0⟩ -- outputs (4.0, 6.0)

We defined two instances: Add Vec2 so we can use +, and ToString Vec2 so Lean can print Vec2 values.

1.4.5.Β Using derivingπŸ”—

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.

structure Student where name : String age : β„• deriving Repr def alice : Student := { name := "Alice", age := 22 } { name := "Alice", age := 22 }#eval alice -- 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)

Several instances can be derived at once:

structure Pair (Ξ± Ξ² : Type) where fst : Ξ± snd : Ξ² deriving Repr, BEq, Hashable

1.4.6.Β The square bracket notationπŸ”—

When a function needs a typeclass instance, we use square brackets [...] in its signature:

def doubleIt [Add Ξ±] (x : Ξ±) : Ξ± := x + x 10#eval doubleIt 5 -- outputs 10 6#eval doubleIt (3 : β„€) -- outputs 6

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:

def tripleIt [inst : Add Ξ±] (x : Ξ±) : Ξ± := inst.add x (inst.add x x)

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.

1.4.7.Β Inspecting typeclass arguments with @πŸ”—

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.

1.4.8.Β The algebraic hierarchy in MathlibπŸ”—

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.

1.4.9.Β Defining multiple instancesπŸ”—

Let us build a more complete example. We define a type Mod3 representing integers modulo 3, and equip it with algebraic structure:

inductive Mod3 where | zero | one | two deriving Repr, BEq instance : Add Mod3 where add | Mod3.zero, b => b | a, Mod3.zero => a | Mod3.one, Mod3.one => Mod3.two | Mod3.one, Mod3.two => Mod3.zero | Mod3.two, Mod3.one => Mod3.zero | Mod3.two, Mod3.two => Mod3.one instance : Mul Mod3 where mul | Mod3.zero, _ => Mod3.zero | _, Mod3.zero => Mod3.zero | Mod3.one, b => b | a, Mod3.one => a | Mod3.two, Mod3.two => Mod3.one instance : Zero Mod3 where zero := Mod3.zero instance : One Mod3 where one := Mod3.one Mod3.one#eval (Mod3.two + Mod3.two : Mod3) -- outputs Mod3.one Mod3.one#eval (Mod3.two * Mod3.two : Mod3) -- outputs Mod3.one

1.4.10.Β How instance resolution worksπŸ”—

When Lean encounters an expression like a + b where a b : Ξ±, it needs to find an instance of Add Ξ±. The resolution proceeds as follows:

  1. Lean looks through all registered instances (those declared with instance).

  2. It tries to unify the goal Add Ξ± with the type of each instance.

  3. If an instance itself requires other instances (e.g., Add (Prod Ξ± Ξ²) might require Add Ξ± and Add Ξ²), Lean recursively resolves those.

  4. 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?":

inferInstance : AddCommMonoid β„•#check (inferInstance : AddCommMonoid β„•)

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.

1.4.11.Β Output parametersπŸ”—

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.

1.4.12.Β The Decidable typeclassπŸ”—

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:

example : 2 + 3 = 5 := ⊒ 2 + 3 = 5 All goals completed! πŸ™ example : Β¬(2 + 3 = 6) := ⊒ Β¬2 + 3 = 6 All goals completed! πŸ™

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:

def decZero (n : Nat) : Decidable (n = 0) := match n with | 0 => isTrue rfl -- a proof that 0 = 0 | m + 1 => isFalse (Nat.succ_ne_zero m) -- 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.

1.4.13.Β CoercionsπŸ”—

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 := b example (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 : β„•) : β„• := f n

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.

1.4.14.Β Where numerals come from (OfNat)πŸ”—

Even a plain numeral is resolved through a typeclass. Writing 2 is sugar for OfNat.ofNat 2:

@OfNat.ofNat : {Ξ± : Type u_1} β†’ (x : β„•) β†’ [self : OfNat Ξ± x] β†’ Ξ±#check @OfNat.ofNat -- {Ξ± : Type} β†’ (n : β„•) β†’ [OfNat Ξ± n] β†’ Ξ±

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:

structure Countdown where val : Nat instance (n : Nat) : OfNat Countdown n where ofNat := ⟨n⟩ 5#eval (5 : Countdown).val -- 5

1.4.15.Β Managing instancesπŸ”—

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.

1.4.16.Β Practical tipsπŸ”—

  • 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).

1.5.Β Propositions and proofsπŸ”—

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.

1.5.1.Β Historical contextπŸ”—

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.

For example, this term-mode proof

example (P : Prop) : False β†’ P := False.elim

is the same as

example (P : Prop) : False β†’ P := P:Prop⊒ False β†’ P All goals completed! πŸ™

and likewise

example (s t : Set ℝ) (hst : s βŠ† t) (x : ℝ) : x ∈ s β†’ x ∈ t := fun hx ↦ hst hx

is the tactic proof

example (s t : Set ℝ) (hst : s βŠ† t) (x : ℝ) : x ∈ s β†’ x ∈ t := s:Set ℝt:Set ℝhst:s βŠ† tx:β„βŠ’ x ∈ s β†’ x ∈ t s:Set ℝt:Set ℝhst:s βŠ† tx:ℝhx:x ∈ s⊒ x ∈ t All goals completed! πŸ™

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:

example : True := True.intro

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):

example (C : Prop) : False β†’ C := fun h => nomatch h

1.5.5.Β Implication = function typeπŸ”—

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 (P Q : Prop) (hP : P) (hPQ : P β†’ Q) : Q := P:PropQ:ProphP:PhPQ:P β†’ Q⊒ Q All goals completed! πŸ™ -- Term proof: just function application! example (P Q : Prop) (hP : P) (hPQ : P β†’ Q) : Q := hPQ hP

A proof of P β†’ Q β†’ R is a function of two arguments (curried):

example (P Q R : Prop) : (P β†’ Q) β†’ (Q β†’ R) β†’ (P β†’ R) := P:PropQ:PropR:Prop⊒ (P β†’ Q) β†’ (Q β†’ R) β†’ P β†’ R intro hPQ P:PropQ:PropR:ProphPQ:P β†’ QhQR:Q β†’ R⊒ P β†’ R P:PropQ:PropR:ProphPQ:P β†’ QhQR:Q β†’ RhP:P⊒ R All goals completed! πŸ™ -- Term proof: composition of functions example (P Q R : Prop) : (P β†’ Q) β†’ (Q β†’ R) β†’ (P β†’ R) := fun hPQ hQR hP => hQR (hPQ hP)

1.5.6.Β Conjunction = product typeπŸ”—

The conjunction P ∧ Q corresponds to the product type P Γ— Q (more precisely, it is defined as a structure with two fields). A proof of P ∧ Q is a pair ⟨hP, hQ⟩ consisting of a proof of P and a proof of Q.

example (P Q : Prop) (hP : P) (hQ : Q) : P ∧ Q := P:PropQ:ProphP:PhQ:Q⊒ P ∧ Q All goals completed! πŸ™ -- Term proof: just construct the pair example (P Q : Prop) (hP : P) (hQ : Q) : P ∧ Q := ⟨hP, hQ⟩ -- Eliminating a conjunction: projections example (P Q : Prop) (h : P ∧ Q) : P := h.1 example (P Q : Prop) (h : P ∧ Q) : Q := h.2

Note that And.intro is the constructor, and And.left / And.right (equivalently .1 / .2) are the projections -- exactly like a product type.

1.5.7.Β Disjunction = sum typeπŸ”—

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 (P Q : Prop) (hP : P) : P ∨ Q := P:PropQ:ProphP:P⊒ P ∨ Q P:PropQ:ProphP:P⊒ P All goals completed! πŸ™ -- Term proof: inject into the left summand example (P Q : Prop) (hP : P) : P ∨ Q := Or.inl hP -- Eliminating a disjunction: case analysis example (P Q R : Prop) (h : P ∨ Q) (hPR : P β†’ R) (hQR : Q β†’ R) : R := h.elim hPR hQR

1.5.8.Β Negation = function to FalseπŸ”—

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⊒ False All goals completed! πŸ™ -- Term proof: function application example (P : Prop) (hP : P) (hnP : Β¬P) : False := hnP hP -- 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 = n n:β„•βŠ’ n = n All goals completed! πŸ™ -- Term proof: a dependent function (lambda) example : βˆ€ (n : β„•), n = n := fun n => 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.

example : βˆƒ (n : β„•), n > 0 := ⊒ βˆƒ n, n > 0 ⊒ 1 > 0; All goals completed! πŸ™ -- Term proof: construct the dependent pair example : βˆƒ (n : β„•), n > 0 := ⟨1, ⊒ 1 > 0 All goals completed! πŸ™βŸ© -- Another example example : βˆƒ (n : β„•), n + n = 10 := ⟨5, ⊒ 5 + 5 = 10 All goals completed! πŸ™βŸ©

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:

@Classical.choice : {Ξ± : Sort u_1} β†’ Nonempty Ξ± β†’ Ξ±#check @Classical.choice

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.

example {α : Type} (a : α) : Nonempty α := ⟨a⟩ noncomputable example {α : Type} (h : Nonempty α) : α := Classical.choice h

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.

example (P Q : Prop) (hP : P) (hQ : Q) : P ∧ Q := P:PropQ:ProphP:PhQ:Q⊒ P ∧ Q Try this: [apply] exact ⟨hP, hQ⟩show_term All goals completed! πŸ™

Here is a more complex example where the term proof is less obvious:

example (P Q R : Prop) : (P ∧ Q) β†’ R β†’ (R ∧ P) := P:PropQ:PropR:Prop⊒ P ∧ Q β†’ R β†’ R ∧ P intro ⟨hP, _⟩ P:PropQ:PropR:ProphP:Pright✝:QhR:R⊒ R ∧ P All goals completed! πŸ™ -- Equivalent term proof example (P Q R : Prop) : (P ∧ Q) β†’ R β†’ (R ∧ P) := fun ⟨hP, _⟩ hR => ⟨hR, hP⟩

1.5.13.Β Summary tableπŸ”—

Here is a summary of the Curry-Howard dictionary:

Logic

Type Theory

Lean notation

Proposition

Type

P : Prop

Proof

Term (inhabitant)

h : P

Implication P β†’ Q

Function type

P β†’ Q

Conjunction P ∧ Q

Product type

P ∧ Q / And P Q

Disjunction P ∨ Q

Sum type

P ∨ Q / Or P Q

True

Unit type

True

False

Empty type

False

Negation Β¬P

Function to empty

P β†’ False

βˆ€ (x : Ξ±), P x

Dependent function (Ξ )

(x : Ξ±) β†’ P x

βˆƒ (x : Ξ±), P x

Dependent pair (Ξ£)

⟨a, ha⟩ : βˆƒ x, P x

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.

1.5.14.Β From correspondence to practiceπŸ”—

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:

theorem declaration uses `sorry`goldbach : βˆ€ (n : β„•) (h₁ : n > 2) (hβ‚‚ : Even n), βˆƒ (i j : β„•), (Prime i) ∧ (Prime j) ∧ (n = i + j) := ⊒ βˆ€ n > 2, Even n β†’ βˆƒ i j, Prime i ∧ Prime j ∧ n = i + j All goals completed! πŸ™

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.

Which tactics, and where to find themπŸ”—

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.

ExercisesπŸ”—

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.

1.6.Β Dependent Types in DepthπŸ”—

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.

1.6.1.Β What makes dependent types specialπŸ”—

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.

1.6.2.Β Pi types: dependent function typesπŸ”—

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:

βˆ€ (n : β„•), n = n : Prop#check (βˆ€ (n : β„•), n = n) -- Prop βˆ€ (n : β„•), n = n : Prop#check ((n : β„•) β†’ n = n) -- same Prop

Here is a concrete example of a dependent function:

def allPositiveSucc : (n : β„•) β†’ 0 < n + 1 := fun n => Nat.succ_pos n

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:

def polyId : (Ξ± : Type) β†’ Ξ± β†’ Ξ± := fun _ a => a polyId β„• 42 : β„•#check polyId β„• 42 -- β„• polyId String "hi" : String#check polyId String "hi" -- String

1.6.3.Β Sigma types: dependent pair typesπŸ”—

The Sigma type (also written Ξ£-type) is the type of dependent pairs. In Lean, we write:

Ξ£ (x : Ξ±), Ξ² x

or equivalently (x : Ξ±) Γ— Ξ² x. A term of this type is a pair ⟨a, b⟩ where a : Ξ± and b : Ξ² a. Note that the type of the second component depends on the value of the first component.

def exampleSigma : { n : β„• // n % 2 = 0 } := ⟨4, ⊒ 4 % 2 = 0 All goals completed! πŸ™βŸ© -- Accessing the components ↑exampleSigma : β„•#check exampleSigma.1 -- β„• exampleSigma.property : ↑exampleSigma % 2 = 0#check exampleSigma.2 -- 4 % 2 = 0

When Ξ² does not depend on x, the Sigma type reduces to the ordinary product type Ξ± Γ— Ξ².

1.6.4.Β Difference between Sigma and ExistsπŸ”—

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.

def sigmaExample : { n : β„• // n > 5 } := ⟨7, ⊒ 7 > 5 All goals completed! πŸ™βŸ© 7#eval sigmaExample.1 -- 7 -- Exists: we CANNOT extract the witness for computation -- (we can only use it inside proofs) example : βˆƒ (n : β„•), n > 5 := ⟨7, ⊒ 7 > 5 All goals completed! πŸ™βŸ©

Mathematically, Ξ£ is like a disjoint union (indexed coproduct), while βˆƒ is the propositional truncation thereof. In practice:

  • Use βˆƒ when you only need to know that a witness exists (typical in mathematics).

  • Use Ξ£ when you need to actually compute with the witness (typical in programming).

1.6.5.Β Vectors indexed by lengthπŸ”—

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).

def myVec : Fin 3 β†’ β„• := ![10, 20, 30] 10#eval myVec 0 -- 10 20#eval myVec 1 -- 20 30#eval myVec 2 -- 30

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.

1.6.6.Β Matrices as dependent typesπŸ”—

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:

def myMatrix : Fin 2 β†’ Fin 3 β†’ β„• := ![![1, 2, 3], ![4, 5, 6]] 2#eval myMatrix 0 1 -- 2 6#eval myMatrix 1 2 -- 6

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.

1.6.7.Β Type familiesπŸ”—

A type family is simply a function that returns a type. Type families are pervasive in Lean and Mathlib:

def BoolFamily : Bool β†’ Type | true => β„• | false => String -- Dependent function using the type family def boolFamilyExample : (b : Bool) β†’ BoolFamily b | 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 Ξ².

def fiber (f : β„• β†’ β„•) (b : β„•) : Type := {a : β„• // f a = b} -- An element of the fiber of (Β· * 2) over 10 def fiberExample : fiber (Β· * 2) 10 := ⟨5, ⊒ (fun x => x * 2) 5 = 10 All goals completed! πŸ™βŸ©

1.6.8.Β SubtypesπŸ”—

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.

def BigNat : Type := {n : β„• // n > 5} def seven : BigNat := ⟨7, ⊒ 7 > 5 All goals completed! πŸ™βŸ© def ten : BigNat := ⟨10, ⊒ 10 > 5 All goals completed! πŸ™βŸ© -- We can extract the value 7#eval seven.val -- 7

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 set s : 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.

1.6.9.Β Why dependent types matterπŸ”—

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:

  1. Length-indexed collections: The type system guarantees that head is never called on an empty list, that matrix dimensions match in multiplication, etc.

  2. Precise specifications: You can write a sorting function whose type guarantees that the output is sorted and is a permutation of the input.

  3. 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.

  4. Safe APIs: You can define a division function (a : β„€) β†’ (b : β„€) β†’ b β‰  0 β†’ β„€ that requires a proof of non-zero denominator. No runtime exceptions possible.

def safeDiv (a b : β„€) (hb : b β‰  0) : β„€ := a / b -- This works: 3#eval safeDiv 10 3 (⊒ 3 β‰  0 All 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.

1.7.Β Notation and abbreviationsπŸ”—

1.7.1.Β Defining new notationπŸ”—

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:

section NotationDemo notation "Ξ΄" => (2 : β„•) 2 : β„•#check (Ξ΄ : β„•)

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:

notation "double" x => x + x 10#eval double 5 -- 10 end NotationDemo

You can also define infix notation with a priority (determining how tightly the operator binds):

section InfixDemo infixl:65 " βŠ•βŠ• " => fun (a b : β„•) => a + b + 1 8#eval 3 βŠ•βŠ• 4 -- 8 end InfixDemo

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.

Prefix and postfix operatorsπŸ”—

prefix and postfix define unary notation on a single argument:

section UnaryDemo -- A prefix "bang" operator, 80 precedence (higher than `+`) prefix:80 "Β‘" => fun n : β„• => n * n 25#eval Β‘5 -- 25 -- A postfix factorial-style operator postfix:90 "!!" => fun n : β„• => 2 * n + 1 9#eval 4!! -- 9 end UnaryDemo

Multi-argument notationπŸ”—

notation can take more than one argument and mix custom tokens with them:

section TernaryDemo -- "between a and b" means the half-open interval [a, b) notation "between " a " and " b => Set.Ico a b between 1 and 10 : Set β„•#check between (1 : β„•) and 10 -- Set β„• end TernaryDemo

Notation with bindersπŸ”—

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.

Scoped notationπŸ”—

If a notation should only be active inside a namespace (so it does not pollute the global symbol space), mark it scoped:

namespace MyDemo scoped notation "✦" => (42 : β„•) end MyDemo -- Outside the namespace, `✦` is not in scope by default; -- enable it with `open MyDemo` or `open scoped MyDemo`. open scoped MyDemo 42#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.

1.7.2.Β Two useful shorthandsπŸ”—

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 β–Έ).

example (P : β„• β†’ Prop) (x y : β„•) (h : x = y) (hx : P x) : P y := P:β„• β†’ Propx:β„•y:β„•h:x = yhx:P x⊒ P y All goals completed! πŸ™

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.

example (P Q : Prop) (hP : P) (hnP : Β¬P) : Q := P:PropQ:ProphP:PhnP:Β¬P⊒ Q All goals completed! πŸ™

1.7.3.Β The abbrev commandπŸ”—

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.

abbrev NatAlias := β„•

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

def NatAlias' := β„•

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).