This part opens with the foundations -- the everyday vocabulary
(propositions, proofs, and sets), the universes and axioms on which
Lean's mathematics rests, and well-foundedness and the avoidance of
paradoxes -- and then surveys parts of Mathlib that go beyond
first-year mathematics: order theory and lattices, the algebraic
hierarchy, filters as a unifying language for limits, topology and
measure theory, and discrete probability. The aim is not to redo the
mathematics from scratch but to show how Mathlib organizes it -- which
typeclasses to use, where to find the lemmas, and what the design
trade-offs were.
Each chapter starts with a short notation table so the symbols are
introduced before they appear, and uses Verso's {docstring} block
to render Mathlib definitions and lemmas inline so the notes stay in
sync with the library.
A mathematician thinks in sets; Lean thinks in types. A set turns
out to be a predicate on a type, its operations the logical
connectives, and its equality an axiom -- the four sections below.
Propositions and their proofs, used freely from here on, were
covered in the Curry-Howard chapter of the
Lean part.
The first reflex to unlearn is the set-theoretic one that everything
is a set. In set theory ∈ is the single primitive, an object can be
a member of many sets, and there is no fixed notion of "the type of
x". Type theory inverts this: the primitive is the type, and every
term a : α has exactly one type, fixed once and for all.
A set in Lean is not a new type. A Set α is a predicate on the
already-existing type α, carving out some of its terms. An element of
s : Set α therefore still has type α; the set records only which
terms of α lie in it.
Three notions are worth keeping firmly apart:
a typeα -- the ambient collection of terms;
a sets : Set α -- a predicate selecting terms of α, whose
elements keep their type α;
a subtype{ x : α // p x } (see Subtypes) -- a
genuinely new type, whose terms are pairs of a value and a proof
that it satisfies p.
So there is a difference between a set and a type, and it is the one
a mathematician must internalize. The rest of this chapter makes it
precise: sets as predicates and their operations, when two sets are
equal, and finally how to cross the gap -- turning a set back into a
type.
The set-theoretic symbols below are unicode characters typed in VS
Code via a backslash escape. Hover over a symbol in the editor to see
its input sequence.
Set α is definitionally the function type α → Prop. The term
a : α belongs to s : Set α exactly when s a holds, and a ∈ s is
notation for that.
-- `Set α` is definitionally `α → Prop`Setℕ : Type#check(Setℕ)example:Setℕ=(ℕ→Prop):=rfl-- Set-builder notation packages a predicate as a setdefEvens:Setℕ:={n|n%2=0}-- Membership unfolds to applying the predicateexample:(4∈Evens)↔(4%2=0):=Iff.rfl-- ...so concrete membership questions are decidableexample:4∈Evens:=⊢ 4∈Evens⊢ 4%2=0All goals completed! 🐙example:3∉Evens:=⊢ 3∉Evens⊢ ¬3%2=0All goals completed! 🐙
Because a set is a predicate, two predicates that hold of exactly
the same elements describe the same set -- set equality is
extensional. We make this precise just below.
The usual Boolean operations on sets are all defined pointwise on the
underlying predicates, so each unfolds to a familiar logical
connective.
-- `s ⊆ t` means: every member of `s` is a member of `t`example(st:Setℕ):(s⊆t)↔∀a,a∈s→a∈t:=Iff.rfl-- the empty set, the universal set, and the operations∅ : Setℕ#check(∅:Setℕ)Set.univ : Setℕ#check(Set.univ:Setℕ)example(st:Setℕ):Setℕ:=s∩t-- intersectionexample(st:Setℕ):Setℕ:=s∪t-- unionexample(s:Setℕ):Setℕ:=sᶜ-- complement
The binary ∪ and ∩ extend to arbitrary families. Given s : ι → Set α -- a set s i for each index i : ι -- the indexed union⋃ i, s i and indexed intersection⋂ i, s i collect members across the whole family.
As always, membership just unfolds to a quantifier:
-- `a ∈ ⋃ i, s i` iff `a` lies in *some* member of the familyexample(s:ℕ→Setℕ)(a:ℕ):(a∈⋃i,si)↔∃i,a∈si:=Set.mem_iUnion-- `a ∈ ⋂ i, s i` iff `a` lies in *every* memberexample(s:ℕ→Setℕ)(a:ℕ):(a∈⋂i,si)↔∀i,a∈si:=Set.mem_iInter-- a concrete union: the singletons cover all of `ℕ`example:(⋃n:ℕ,{n})=Set.univ:=⊢ ⋃n,{n}=Set.univa:ℕ⊢ a∈⋃n,{n}↔a∈Set.univ;All goals completed! 🐙
A common variant restricts the index to a seta : Set ι rather than the whole index type -- written ⋃ i ∈ a, s i and ⋂ i ∈ a, s i. This is just the indexed operation ranging over the members of a, and membership carries the bound along:
-- union over `i` ranging in a set `a`example(a:Setℕ)(s:ℕ→Setℕ)(x:ℕ):(x∈⋃i∈a,si)↔∃i∈a,x∈si:=a:Setℕs:ℕ→Setℕx:ℕ⊢ x∈⋃i∈a,si↔∃i∈a,x∈siAll goals completed! 🐙-- and the bounded intersectionexample(a:Setℕ)(s:ℕ→Setℕ)(x:ℕ):(x∈⋂i∈a,si)↔∀i∈a,x∈si:=a:Setℕs:ℕ→Setℕx:ℕ⊢ x∈⋂i∈a,si↔∀i∈a,x∈siAll goals completed! 🐙
When the family is given not by an index but as a set of setsS : Set (Set α), the same operations are written ⋃₀ S (sUnion) and ⋂₀ S (sInter):
-- `a ∈ ⋃₀ S` iff `a` lies in some `t ∈ S`example(S:Set(Setℕ))(a:ℕ):(a∈⋃₀S)↔∃t∈S,a∈t:=Set.mem_sUnion
These are exactly the set/indexed suprema and infima of the complete lattice of sets: ⋃ i, s iisiSup s, and ⋃₀ SissSup S. They are also what σ-algebras are closed under -- a countable union or intersection is just the indexed one over ι = ℕ.
Two sets are equal exactly when they have the same members: the set
extensionality principle Set.ext. This is not a definition but a
consequence of the axioms -- a set is a function into Prop, so it
rests on the extensionality axioms taken up in
the next chapter. Here we simply use it.
-- equality of sets is membership-wise equalityexample(st:Setℕ):s=t↔∀a,a∈s↔a∈t:=⟨funha=>s:Setℕt:Setℕh:s=ta:ℕ⊢ a∈s↔a∈tAll goals completed! 🐙,funh=>Set.exth⟩-- the `ext` tactic reduces a set equality to membershipexample:{n:ℕ|n%2=0}={n|2∣n}:=by⊢ {n|n%2=0}={n|2∣n}extnn:ℕ⊢ n∈{n|n%2=0}↔n∈{n|2∣n}simponly[`Set.mem_setOf_eq` has been deprecated: Use `Set.mem_ofPred_eq` insteadSet.mem_setOf_eq]n:ℕ⊢ n%2=0↔2∣nconstructormpn:ℕ⊢ n%2=0→2∣nmprn:ℕ⊢ 2∣n→n%2=0<;>mpn:ℕ⊢ n%2=0→2∣nmprn:ℕ⊢ 2∣n→n%2=0introhmprn:ℕh:2∣n⊢ n%2=0<;>mpn:ℕh:n%2=0⊢ 2∣nmprn:ℕh:2∣n⊢ n%2=0omegaAll goals completed! 🐙
2.1.7. From a set back to a type: subtypes and coercion🔗
A set is not a type, and sometimes you need one: to speak of a function
defined only on s, or to sum over its elements. A set is promoted
to a type by coercion: ↥s denotes the subtype
{ x // x ∈ s } of its members, a genuinely new type whose terms pair
an element of α with a proof of membership.
-- coercing a set to a type gives the subtype of members↑Evens : Type#check(↥Evens)-- a Typeexample:↥Evens={x:ℕ//x∈Evens}:=rfl-- an element of the subtype pairs a value with a proofexample:↥Evens:=⟨4,by⊢ 4∈Evensshow4%2=0⊢ 4%2=0;decideAll goals completed! 🐙⟩
With sets, subtypes, and their relationship to types in hand, we are
ready to look at the structures Mathlib builds on top of them.
Lean's type theory rests on a carefully designed system of universes, a small number of axioms, and a powerful logic that can be used both constructively and classically. In this chapter, we examine these foundational aspects.
The universe hierarchy was given in Part 1: Prop = Sort 0, Type = Type 0 = Sort 1, Type u = Sort (u+1), the rule Sort u : Sort (u + 1), and universe-polymorphism via Type*. We now show why the absence of Type : Type is not optional. Were Type : Type to hold, one could reproduce Russell's paradox at the level of types -- Girard's paradox -- and thereby prove False. The stratification
Type : Type 1#check(Type0:Type1)-- Type : Type 1Type 1 : Type 2#check(Type1:Type2)-- Type 1 : Type 2
is exactly what blocks this, and it is what keeps the whole system consistent. Everything else in this chapter -- the axioms, proof irrelevance, the CIC -- rests on top of it.
How the collapse would actually unfold -- building Girard's paradoxical universe in Lean, watching exactly where the type-checker stops it, and deriving False from the hypothesis that the stratification were absent -- is not treated here: it is the subject of Project 1 in the Projects chapter.
Prop differs from every Type n by proof irrelevance (see why Prop is special): if P : Prop and h1 h2 : P, then h1 = h2. What follows is the foundational side of that.
This is a deliberate design choice. It means:
You cannot pattern-match on a proof to extract computational data.
Proofs can be erased at compile time without affecting program behavior.
Two mathematical proofs of the same theorem are interchangeable.
That any two proofs of the same P : Prop are equal is itself
provable by rfl and costs no axiom; this, and the reason an
existential therefore lives in Prop -- so that recovering its witness
needs choice -- are taken up in
Declared axioms vs. what the kernel bakes in below.
The distinction between Prop and Type corresponds to the mathematical distinction between "asserting that something exists" (Prop) and "constructing a specific example" (Type).
The universe hierarchy, Prop with its proof irrelevance, dependent functions and inductive types form a single formal system, and that system is what Lean's kernel implements (de Moura and Ullrich, 2021)Leonardo de Moura and Sebastian Ullrich, 2021. “The Lean 4 Theorem Prover and Programming Language”. In Automated Deduction – CADE 28. (Lecture Notes in Computer Science 12699) -- the Calculus of Inductive Constructions, or CIC. The CIC is the entire trusted core of Lean: every definition, theorem, and tactic ultimately elaborates down to terms that the kernel re-checks against these rules, and nothing outside them is trusted.
The CIC is built from exactly three ideas:
Dependent functions -- the "Constructions". This is the Calculus of Constructions of Coquand and Huet (1988)Thierry Coquand and Gérard Huet (1988). “The Calculus of Constructions”. Information and Computation.76(2–3), pp. 95–120.: a typed lambda calculus in which the type of the output may depend on the value of the input, giving the Π-types written (x : α) → β x. In one stroke this subsumes ordinary functions, polymorphic functions (which take types as arguments), and type-level functions.
Inductive types -- the "Inductive", added by Coquand and Paulin-Mohring (1990)Thierry Coquand and Christine Paulin-Mohring, 1990. “Inductively Defined Types”. In COLOG-88. (Lecture Notes in Computer Science 417). This is the rule that introduces a new type by listing its constructors. Both data and logic are built from it: Nat, List, and Prod are inductive types, but so are the logical connectives And, Or, False, and even equality Eq.
-- Data: `Nat` is an inductive type with two constructorsNat.zero : ℕ#check@Nat.zero-- NatNat.succ : ℕ→ℕ#check@Nat.succ-- Nat → Nat-- Logic too: `And`, `Or`, `Eq` are *also* inductive types@And.intro : ∀{ab:Prop},a→b→a∧b#check@And.intro@Or.inl : ∀{ab:Prop},a→a∨b#check@Or.inl@Eq.refl : ∀{α:Sort u_1}(a:α),a=a#check@Eq.refl-- Every inductive type comes with a recursor, which the-- kernel generates automatically; it is how induction-- and recursion are justified in the first place:@Nat.rec : {motive:ℕ→Sort u_1}→motiveNat.zero→((n:ℕ)→motiven→motiven.succ)→(t:ℕ)→motivet#check@Nat.rec
Defining your own inductive type uses exactly the same mechanism the standard library does -- there is no special magic reserved for the built-in types:
inductiveTrafficwhere|red|amber|green
A universe hierarchy (Prop, Type 0, Type 1, ...), exactly as in the first section, which keeps the system logically consistent by forbidding Type : Type.
On top of these three ingredients the kernel bakes in one thing that set theory has no notion of: computation. Terms reduce, so (fun n => n + 1) 2 and 3 are not merely provably equal but definitionally equal, and the kernel checks this by evaluating. This is the same reduction that makes Lean.Parser.Command.eval : command`#eval e` evaluates the expression `e` by compiling it and running the compiled code. It then
prints the resulting value.
* The command attempts to use `ToExpr`, `Repr`, or `ToString` instances to print the result.
* If `e` is a monadic value of type `m ty`, then the command tries to adapt the monad `m`
to one of the monads that `#eval` supports, which include `IO`, `CoreM`, `MetaM`, `TermElabM`, and `CommandElabM`.
Users can define `MonadEval` instances to extend the list of supported monads.
The `#eval` command gracefully degrades in capability depending on what is imported.
Importing the `Lean.Elab.Command` module provides full capabilities.
Due to unsoundness, `#eval` refuses to evaluate expressions that depend on `sorry`, even indirectly,
since the presence of `sorry` can lead to runtime instability and crashes.
This check can be overridden with the `#eval! e` command.
Options:
* If `eval.pp` is true (default: true) then tries to use `ToExpr` instances to make use of the
usual pretty printer. Otherwise, only tries using `Repr` and `ToString` instances.
* If `eval.type` is true (default: false) then pretty prints the type of the evaluated value.
* If `eval.derive.repr` is true (default: true) then attempts to auto-derive a `Repr` instance
when there is no other way to print the result.
See also: `#reduce e` for evaluation by term reduction.
#eval and the decide tactic work, and it is what lets us speak of computable functions at all.
The CIC is small. A type checker for it is a few thousand lines of code, and that program is the whole of what you must trust in order to believe a Lean proof. Everything in Mathlib -- the integers, the real numbers, manifolds, schemes -- is in the end a tower of inductive types and dependent functions resting on this kernel.
The CIC by itself is minimal and constructive: a handful of principles that working mathematicians use without a second thought cannot be proved in it. Lean therefore adds axioms on top of the kernel. There are exactly three, and we can ask Lean itself to confirm it with the #print axioms command, which traces a constant back to the axioms it ultimately depends on:
'propext' depends on axioms: [propext]#printaxiomspropext-- 'propext' depends on axioms: [propext]'Quot.sound' depends on axioms: [Quot.sound]#printaxiomsQuot.sound-- 'Quot.sound' depends on axioms: [Quot.sound]'Classical.choice' depends on axioms: [Classical.choice]#printaxiomsClassical.choice-- 'Classical.choice' depends on axioms: [Classical.choice]
Each of the three depends only on itself -- the signature of a genuine axiom. Everything else that looks axiomatic -- function extensionality, the law of excluded middle, the set-theoretic axiom of choice -- is in fact a theorem derived from these three, as we verify below. We take the three axioms first.
Two propositions are equal if and only if they are logically equivalent:
@propext : ∀{ab:Prop},(a↔b)→a=b#check@propext-- propext : ∀ {a b : Prop}, (a ↔ b) → a = b-- Example: P ∧ True is the same proposition as Pexample(P:Prop):(P∧True)=P:=byP:Prop⊢ (P∧True)=PextP:Prop⊢ P∧True↔PsimpAll goals completed! 🐙
This axiom is exactly what makes set equalityextensional: since Set α is α → Prop, propext is what lets predicates that are logically equivalent count as the same set.
Lean has built-in support for quotient types (introduced in the Types chapter). Given a type α and an equivalence relation r on α, you can form the quotient type Quot r. The axiom Quot.sound says that if r a b, then Quot.mk r a = Quot.mk r b.
This is how many mathematical constructions are formalized: the integers as a quotient of ℕ × ℕ, the rationals as a quotient of ℤ × ℤ, the reals as a quotient of Cauchy sequences, and so on.
-- Quotient types are built into Lean@Quot.mk : {α:Sort u_1}→(r:α→α→Prop)→α→Quotr#check@Quot.mk@Quot.sound : ∀{α:Sort u_1}{r:α→α→Prop}{ab:α},rab→Quot.mkra=Quot.mkrb#check@Quot.sound@Quot.lift : {α:Sort u_1}→{r:α→α→Prop}→{β:Sort u_2}→(f:α→β)→(∀(ab:α),rab→fa=fb)→Quotr→β#check@Quot.lift
Function extensionality is proved from quotient soundness; see below.
Given a proof that a type is nonempty (i.e., inhabited), Classical.choice produces an actual element. This is a non-constructive axiom: it asserts existence without providing any recipe for the element. From it one also recovers the more familiar set-theoretic axiom of choice and Hilbert's ε-operator.
2.2.5. What is not an axiom: funext and excluded middle🔗
Two principles that mathematicians treat as bedrock are not axioms in Lean. They are theorems derived from the three above.
Two functions are equal if they agree on every input -- which, together with propext, is what makes set equality extensional (two sets with the same members are equal because they are equal functions into Prop). In practice one uses it through the funext lemma and tactic:
-- funext: if f x = g x for all x, then f = g@funext : ∀{α:Sort u_1}{β:α→Sort u_2}{fg:(x:α)→βx},(∀(x:α),fx=gx)→f=g#check@funext-- funext : ∀ {α : Sort u} {β : α → Sort v} {f g : (x : α) → β x},-- (∀ (x : α), f x = g x) → f = gexample:(funn:ℕ=>n+0)=(funn:ℕ=>n):=by⊢ (funn=>n+0)=funn=>nfunextnn:ℕ⊢ n+0=nsimpAll goals completed! 🐙
Despite the name and its fundamental role, funext is not an axiom: it is provable from quotient soundness. Lean confirms that its only axiomatic dependency is Quot.sound:
'funext' depends on axioms: [Quot.sound]#printaxiomsfunext-- 'funext' depends on axioms: [Quot.sound]
Likewise the law of excluded middle ∀ p, p ∨ ¬p is not an axiom. It follows from the three axioms by a classical argument known as Diaconescu's theorem: the axiom of choice implies excluded middle. Lean again makes the dependency explicit:
Classical.em : ∀(p:Prop),p∨¬p#check@Classical.em-- Classical.em : ∀ (p : Prop), p ∨ ¬p'Classical.em' depends on axioms: [propext,Classical.choice,Quot.sound]#printaxiomsClassical.em-- 'Classical.em' depends on axioms:-- [propext, Classical.choice, Quot.sound]
The argument is worth seeing, because it pinpoints exactly where non-constructivity enters. Fix a proposition p and form two predicates on Prop:
U x := (x = True) ∨ p
V x := (x = False) ∨ p
Both are nonempty -- True satisfies U and False satisfies V -- so Classical.choice hands us concrete witnesses u v : Prop together with proofs of u = True ∨ p and v = False ∨ p. Now case-split on those two disjunctions:
If either disjunction takes its right branch, we have a proof of p outright -- the left side of p ∨ ¬p.
Otherwise u = True and v = False, so u ≠ v. But were p true, then propext would collapse U and V into the same predicate, forcing the chosen witnesses to coincide, u = v -- a contradiction. Hence ¬p, the right side.
Either way we obtain p ∨ ¬p. The decisive step is that Classical.choice turns the propositional assumption p into comparable data, the booleans True and False, which is the move a constructive system refuses. Mathlib packages the result as Classical.em, used in practice through by_contra or by_cases.
The step "assume p, derive a contradiction, conclude ¬p" does not itself use excluded middle, so the argument is not circular. Proving ¬p by assuming p and reaching a contradiction is negation introduction -- it is simply the definition of ¬p as p → False, and it is constructively valid. The classical principle is the opposite direction: concluding a positive p from ¬¬p (that is by_contra), which we never invoke here. Likewise, the case splits above eliminate disjunctions we already hold (from the choice specification); they do not conjure p ∨ ¬p out of nothing -- that disjunction is our conclusion, assembled constructively from the cases. The one and only non-constructive ingredient is Classical.choice, and using it is not the same as assuming the p ∨ ¬p we are proving.
2.2.6. Declared axioms vs. what the kernel bakes in🔗
Saying Lean has "exactly three axioms" is precise about declared axioms -- the axiom constants, which are exactly what #print axioms reports. But the kernel's typing and definitional-equality rules quietly build in further logical principles that are not axioms and leave no trace in #print axioms. Unlike the three axioms, you cannot opt out of them.
The clearest are definitional: they hold by rfl, yet cost nothing.
-- Each holds by `rfl` -- it is *definitional* -- and none-- of them costs an axiom.theorempf_irrel(p:Prop)(h1h2:p):h1=h2:=rfltheoremeta_fun(f:Nat→Nat):(funx=>fx)=f:=rfltheoremquot_red(a:Nat)(f:Nat→Nat)(h):Quot.liftfh(Quot.mk(fun__=>True)a)=fa:=rfl'pf_irrel' does not depend on any axioms#printaxiomspf_irrel-- 'pf_irrel' does not depend on any axioms
So the following are all built into the kernel, not assumed on top of it:
Computation (beta, iota, delta, zeta reduction): the kernel evaluates terms, so 2 + 2 = 4 holds by rfl (these reduction rules are detailed in the appendix).
Native literal arithmetic: the kernel computes Nat, Int, and String literals with bignum-backed operations rather than unary succ -- a baked-in optimization you rely on every time you decide or #eval.
Impredicative Prop: ∀ (α : Type), p α is again a Prop, no matter how large α is.
Inductive types and their recursors: every inductive declaration generates an eliminator together with its built-in computation (iota) rule (see the CIC section).
The universe hierarchy (the first section): in particular, never Type : Type.
Subsingleton (large) elimination: you may eliminate a Prop into Type only when it is a subsingleton -- False, Eq, And, Acc qualify, but Or and Exists do not, because they would let you extract genuine data (which side, which witness) from a mere proof.
That last rule is exactly why an existential lives in Prop and you need choice to get its witness:
-- `False` is a subsingleton: large elimination is fineFalse.elim : False→ℕ#check(False.elim:False→Nat)-- `Exists` carries data (its witness), so it may NOT-- be eliminated into `Type`: the witness can't escape.#check_failure(fun(h:∃Variable name `n` is not explicitly referenced.Hint: The binding can be removed (if unused) or named `_` (if used implicitly). Alternatively, prefix the name with `_` to silence this warning:[apply]_nNote: This linter can be disabled with `set_option linter.unusedVariables false`n:Nat,True)=>Exists.rec(motive:=fun_=>Type mismatchℕhas typeTypeof sort `Type 1` but is expected to have typePropof sort `Type`Nat)(funw_=>w)h)-- `Or` has two constructors -- likewise no data eliminationfunh=>Or.rec(funx=>sorry)(funx=>sorry)h : 0=0∨1=1→sorry#check_failure(fun(h:(0=0)∨(1=1))=>Or.rec(motive:=fun_=>Type mismatchℕhas typeTypeof sort `Type 1` but is expected to have typePropof sort `Type`Nat)(fun_=>0)(fun_=>1)h)
First, #print axioms sees only the declared axioms. The baked-in principles are invisible to it and cannot be switched off, so the trusted base of a Lean proof is not "three axioms" but the whole kernel -- the CIC together with proof irrelevance, eta, the quotient reduction, native computation, and the universe rules. Believing a Lean theorem means believing all of that.
Second, proof irrelevance for Prop is effectively uniqueness of identity proofs (axiom K): all proofs of a = b are equal. This is a deliberate design choice -- but it is precisely what makes Lean incompatible with the univalence axiom of Homotopy Type Theory, where distinct equality proofs must be allowed to differ.
By default, Lean's type theory is constructive: proofs must provide explicit witnesses and constructions. The axiom of choice introduces classical reasoning, which allows non-constructive arguments.
We saw above that excluded middle is a theorem, not an axiom (see Diaconescu's theorem). What makes it indispensable in everyday proofs is that it licenses a case split on any proposition, and with it classical staples such as double-negation elimination:
A proposition is Decidable when an
algorithm computes whether it holds, which is what makes
if P then _ else _ and the decide tactic run without any axioms. Classical logic extends this
reach. When you open Classical, every Prop becomes
Decidable -- but non-constructively, via Classical.choice, so the
instance carries no algorithm. This is convenient in proofs (you may
by_cases on any proposition), but code that relies on it is no longer
executable. Decidability is thus the exact dividing line between the
constructive core and the classical extension.
Most mathematicians are (at least implicitly) trained in ZFC set theory. Here are the key differences from Lean's type theory:
Feature
ZFC
Lean
Foundation
Sets and membership ∈
Types and terms :
Everything is a...
Set
Term of some type
3 ∈ 7
Well-formed (and true!)
Type error
Functions
Sets of ordered pairs
Primitive notion
Equality
Between any two sets
Only within a type
Proof objects
Not part of the theory
First-class citizens
Computation
Not built in
Built in (terms reduce)
Type checking
Not applicable
Decidable (in practice)
A key practical difference: in ZFC, you can ask nonsensical questions like "is 3 ∈ π?" and get an answer. In Lean, comparing objects of different types is a type error, which catches many mistakes early.
Both foundations are equiconsistent for most of mathematics. Nearly everything that can be formalized in ZFC can also be formalized in Lean's type theory (and vice versa).
Simply typed lambda calculus (Church, 1940): No dependent types. Foundation for Haskell, OCaml.
System F (Girard, Reynolds, 1970s): Adds polymorphism but not full dependent types. (Girard, 1972)Jean-Yves Girard, 1972. Interprétation fonctionnelle et élimination des coupures de l'arithmétique d'ordre supérieur. PhD thesis, Université Paris VII
Martin-Löf Type Theory (MLTT, 1971+): The origin of dependent types in proof assistants. Used in Agda. (Martin-Löf, 1975)Per Martin-Löf, 1975. “An Intuitionistic Theory of Types: Predicative Part”. In Logic Colloquium '73. (Studies in Logic and the Foundations of Mathematics 80)
Calculus of Inductive Constructions (CIC): the system Lean's kernel implements, described earlier in this chapter; also used, in a closely related form, by Coq/Rocq.
Homotopy Type Theory (HoTT, 2013): Interprets types as topological spaces and equality as paths. Adds the univalence axiom (due to Voevodsky): equivalent types are equal. This is incompatible with Lean's proof irrelevance for Prop, so HoTT uses different proof assistants (Agda, cubical Agda, or specialized Lean forks).
For this course, Lean's type theory (CIC + proof irrelevance + quotients + choice) is the framework we work in. It is expressive enough to formalize virtually all of modern mathematics, as the Mathlib library demonstrates.
A recurring worry in the foundations of mathematics is vicious
circularity: infinite descending chains that make recursion
meaningless, or self-referential sets like Russell's {x | x ∉ x}.
Lean rules these out, but -- importantly -- by two different
mechanisms that are worth keeping apart. This short chapter looks at
both.
The first mechanism lives inside a type and governs recursion. A
relation r : α → α → Prop is well-founded when there are no
infinite descending chains … r a₂ r a₁ r a₀ -- following
r-predecessors downward, you always hit bottom after finitely many
steps.
Why care? Well-foundedness is exactly the condition that makes
recursion and induction legitimate. If f x is defined in terms of
f y for r-smaller y, the computation terminates only because
you cannot descend forever; and the induction principle "to prove
P x, assume P y for every y with r y x" is valid precisely
when r is well-founded. In one phrase: well-founded means "you may
recurse and induct along r".
Lean expresses this through the inductive predicate Acc
("accessible"):
inductive Acc (r : α → α → Prop) : α → Prop where
| intro (x : α) (h : ∀ y, r y x → Acc r y) : Acc r x
Read the constructor as: x is accessible once all of its
r-predecessors y (those with r y x) are themselves accessible.
A relation is well-founded when every element is accessible:
Acc is an inductive type -- the
least fixed point, so it is finitely generated. The constructor
for Acc r x demands accessibility of every predecessor, so the
only way to build the proof is for every downward branch to bottom
out after finitely many steps: a proof of Acc r x is a finite
tree. An infinite descending chain would force an infinitely deep
proof, which no inductive type admits -- so, where every element is
accessible, no such chain exists. (Were Acc instead coinductive,
the greatest fixed point, infinite proofs would be allowed and
every element would count as accessible; the definition would be
vacuous. The induction is the whole point.) This is exactly what
powers well-founded recursion (WellFounded.fix): you may recurse
along r because every descent terminates, and termination_by /
decreasing_by use precisely this.
It helps to see what fails. These relations are not
well-founded:
< on the integers ℤ: the chain 0 > -1 > -2 > … descends
forever, with no bottom.
< on ℚ or ℝ: 1 > ½ > ¼ > … never terminates.
greater-than> on ℕ: the chain 0 < 1 < 2 < … is an infinite
>-descending chain.
For none of these can Lean offer well-founded recursion -- there is no
terminating direction to recurse in. The contrast between < and >
on ℕ is the lesson: less-than bottoms out at 0, greater-than runs
off to infinity.
Concretely, this abstract picture is realized in Lean in two ways.
Each inductive type comes with an auto-generated recursor
(Nat.rec, List.rec, ...) that is its structural induction and
recursion principle -- well-foundedness for that type, guaranteed by
the kernel. And for a uniform supply of well-founded relations,
every inductive type carries a SizeOf instance, which sizeOfWFRel
turns into a WellFoundedRelation whose well-foundedness is pulled
back from < on Nat through InvImage.wf. So "inductive, hence
finite trees, hence well-founded" becomes, in practice, the single
fact Nat.lt_wfRel.wf transported along sizeOf.
n + 1 is accessible because every m < n + 1 (that is, m ≤ n)
is accessible by the induction hypothesis.
The deeper reason is that Nat is itself an inductive type
(zero | succ): every natural number is a finite tower of succ
over zero, so there is no infinite natural number to descend
through. Structural recursion on Nat is the prototype of
well-founded recursion, and the well-foundedness of < is just a
restatement of it.
In practice you almost never prove well-foundedness yourself. Mathlib
already supplies it -- the Nat.lt_wfRel.wf above is one of many
WellFoundedRelation instances -- and when you write a recursive
definition with termination_by (see the Function definitions
chapter in Part 1), Lean finds the well-founded relation automatically
and asks you only, via decreasing_by, to show that your measure
decreases. The induction
argument just given is the one-time justification the library
performs, not something you repeat; you would do it by hand only for a
genuinely new, exotic relation.
The second mechanism is typing, and it is what blocks
self-membership and Russell's paradox. Unlike ZFC, Lean has no global
membership relation: ∈ is typed. The instance behind it,
says that x ∈ s requires x : α and s : Set α. For s ∈ s the
same s would need type αand type Set α at once -- that is,
α = Set α, i.e. α = (α → Prop) (recall that a
set is just a predicate). No such type
exists (there is no
matching Membership instance, and by Cantor's theoremα ≄ (α → Prop)). So s ∈ s is not a false statement; it is
ill-formed:
defmySet:Setℕ:={0}-- `#check_failure` succeeds exactly because the term-- below fails to elaborate -- `mySet ∈ mySet` is a type-- error, not a refutable proposition.#check_failure(failed to synthesize instance of type classMembership(Setℕ)(Setℕ)Hint: Type class instance resolution failures can be inspected with the `set_option trace.Meta.synthInstance true` command.mySet∈mySet)
Consequently Russell's set {x | x ∉ x} cannot even be written in
Lean. Compare ZFC (see the ZFC comparison),
where s ∈ sis a well-formed proposition and is excluded only by
the separate axiom of foundation. Lean rules it out one step
earlier, by typing.
Cantor's theorem, invoked above, states that for any type α there
is no surjection α → Set α: the type of subsets
Set α = α → Prop is always strictly larger than α itself
(Mathlib's Function.cantor_surjective). In particular α and
α → Prop can never be the same type, which is exactly what rules
out α = Set α. The proof is a diagonal argument: given any
candidate surjection f : α → Set α, the "diagonal" subset
{x | x ∉ f x} is hit by no f x at all -- if f a were equal to
it, then a ∈ f a and a ∉ f a would coincide. That diagonal subset
is precisely Russell's {x | x ∉ x}: Cantor's theorem and Russell's
paradox are the same construction, with Cantor drawing the consistent
conclusion (no such surjection exists) exactly where naive set theory
runs into a contradiction.
The very same typing discipline, one level up, forbids Type : Type:
were Type to contain itself, Girard's paradox -- a type-theoretic
Russell -- would make every proposition provable, and the
universe hierarchy is exactly what rules it
out. Building Girard's term in Lean and tracing where predicativity
stops it is one of the course projects.
It is worth holding the two mechanisms apart -- they forbid
circularity at different places:
Phenomenon ruled out
Mechanism
Infinite descent; non-terminating recursion
Acc / WellFounded -- an inductive predicate within a type
s ∈ s, {x | x ∉ x}, Type : Type
Typing and the universe hierarchy -- the structure of the system
Both express the intuition "no self-swallowing circles", but the first
is a theorem-level tool you invoke for recursion, while the second
is baked into the well-formedness rules of the language.
Order structures run through all of mathematics: partial orders in set
inclusion, in divisibility of natural numbers and in subgroup relations;
lattices wherever meets and joins exist; complete lattices in fixed-point
theorems and in topology. Mathlib encodes them as a
hierarchy of typeclasses, and the aim here is to say which assumption each
result actually needs.
A partial order on a type α is a reflexive, transitive, and antisymmetric
relation ≤. In Mathlib this is captured by the typeclass PartialOrder α,
which extends Preorder α (reflexive and transitive) with antisymmetry.
Key lemmas:
le_refl a : a ≤ a
le_trans : a ≤ b → b ≤ c → a ≤ c
le_antisymm : a ≤ b → b ≤ a → a = b
A linear order (LinearOrder α) additionally satisfies totality: for every
a b : α, either a ≤ b or b ≤ a. The natural numbers, integers, and
reals all carry linear orders.
-- Lean already knows that ℕ is linearly ordered:inferInstance : LinearOrderℕ#check(inferInstance:LinearOrderℕ)-- Every linear order is a partial order:inferInstance : PartialOrderℕ#check(inferInstance:PartialOrderℕ)-- A simple proof using transitivityexample(abc:ℕ)(hab:a≤b)(hbc:b≤c):a≤c:=le_transhabhbc-- Antisymmetryexample(ab:ℕ)(hab:a≤b)(hba:b≤a):a=b:=le_antisymmhabhba
The strict order < is defined in terms of ≤:
a < b ↔ a ≤ b ∧ a ≠ b (or equivalently a ≤ b ∧ ¬ b ≤ a).
For any type α, the type Set α carries a partial order given by set
inclusion ⊆. This is a canonical example of a partial order that is not
linear in general.
A lattice is a partial order in which every pair of elements has a least
upper bound (supremum, join) and a greatest lower bound (infimum, meet).
In Mathlib:
⊔ (typed \sup) denotes the join (supremum of two elements).
⊓ (typed \inf) denotes the meet (infimum of two elements).
The typeclass is Lattice α.
Key API:
le_sup_left : a ≤ a ⊔ b
le_sup_right : b ≤ a ⊔ b
sup_le : a ≤ c → b ≤ c → a ⊔ b ≤ c
inf_le_left : a ⊓ b ≤ a
inf_le_right : a ⊓ b ≤ b
le_inf : c ≤ a → c ≤ b → c ≤ a ⊓ b
-- Sets form a lattice with ∪ as ⊔ and ∩ as ⊓example(AB:Setℕ):A⊆A∪B:=Set.subset_union_leftexample(ABC:Setℕ)(hA:C⊆A)(hB:C⊆B):C⊆A∩B:=Set.subset_interhAhB-- In a general lattice:example{α:Type*}[Latticeα](abc:α)(ha:a≤c)(hb:b≤c):a⊔b≤c:=sup_lehahbexample{α:Type*}[Latticeα](ab:α):a⊓b≤a:=inf_le_left
A complete lattice is a partial order in which every subset has a supremum
and an infimum (not just pairs). In Mathlib the typeclass is
CompleteLattice α.
The operations are:
sSup S : the supremum of a set S : Set α
sInf S : the infimum of a set S : Set α
⊤ (\top) : the greatest element (= sSup Set.univ)
⊥ (\bot) : the least element (= sInf Set.univ)
iSup and iInf for indexed suprema and infima.
-- Set α is a complete latticeinferInstance : CompleteLattice(Setℕ)#check(inferInstance:CompleteLattice(Setℕ))-- sSup of a family of sets is their unionexample(S:Set(Setℕ)):sSupS=⋃₀S:=rfl-- Every element of the family is below the sSupexample{α:Type*}[CompleteLatticeα](s:Setα)(a:α)(ha:a∈s):a≤sSups:=le_sSupha
A function f : α → β between preorders is monotone if a ≤ b → f a ≤ f b,
and antitone if a ≤ b → f b ≤ f a. Mathlib provides Monotone f and
Antitone f.
example:Monotone(funn:ℕ↦n+1):=fun__h↦Nat.add_le_add_righth1-- A constant function is both monotone and antitoneexample{αβ:Type*}[Preorderα][Preorderβ](c:β):Monotone(fun_:α↦c):=fun___↦le_reflc
There is also StrictMono f for strictly increasing functions.
The Knaster-Tarski theorem is one of the cornerstones of fixed point
theory. It generalizes Tarski's earlier fixed point theorem for the
real line and has applications in logic, denotational semantics, and
set theory (e.g. the Schröder-Bernstein theorem).
Theorem (Knaster-Tarski). Let α be a complete lattice and let
f : α → α be a monotone function. Then:
f has a least fixed point, given by sInf {x | f x ≤ x}.
f has a greatest fixed point, given by sSup {x | x ≤ f x}.
Moreover, the set of fixed points of f is itself a complete
lattice under the induced order.
The proof of the first part is short and instructive. Let
S = {x | f x ≤ x} (the prefixed points) and set a = sInf S.
Since f is monotone and a ≤ x for every x ∈ S, we get
f a ≤ f x ≤ x for all x ∈ S, so f a is a lower bound of S,
hence f a ≤ a. Thus a ∈ S. Applying f monotonically gives
f (f a) ≤ f a, so f a ∈ S, hence a ≤ f a. Antisymmetry yields
f a = a, and any other fixed point y = f y belongs to S, so
a ≤ y.
In Mathlib this theorem is available through the OrderHom API. The
least fixed point is OrderHom.lfp and the greatest is OrderHom.gfp:
Note that α →o α is the type of monotone self-maps of α (the arrow
→o denotes an OrderHom). A useful small example:
-- The lfp of the constant ∅ map on Set X is ∅:example{X:Type*}:OrderHom.lfp⟨(fun_:SetX↦(∅:SetX)),fun___↦le_refl_⟩=∅:=byX:Type u_1⊢ OrderHom.lfp{toFun:=funx=>∅,monotone':=⋯}=∅applyle_antisymmaX:Type u_1⊢ OrderHom.lfp{toFun:=funx=>∅,monotone':=⋯}⊆∅aX:Type u_1⊢ ∅⊆OrderHom.lfp{toFun:=funx=>∅,monotone':=⋯}·aX:Type u_1⊢ OrderHom.lfp{toFun:=funx=>∅,monotone':=⋯}⊆∅exactOrderHom.lfp_le_(le_refl_)All goals completed! 🐙·aX:Type u_1⊢ ∅⊆OrderHom.lfp{toFun:=funx=>∅,monotone':=⋯}exactbot_leAll goals completed! 🐙
Applications.
Recursive definitions. The semantics of recursion in programming
languages is given by least fixed points of monotone functionals on
a domain of partial functions.
The Schröder-Bernstein theorem. Given injections f : α → β and
g : β → α, one constructs a bijection by taking the least fixed
point of a certain monotone operator on Set α.
Inductively defined sets. Many sets in mathematics (the natural
numbers, syntactic terms, well-founded relations) are defined as
least fixed points of monotone operators.
Here is a quick reference for the most important order-theoretic lemmas:
Lemma
Statement
le_refl
a ≤ a
le_trans
a ≤ b → b ≤ c → a ≤ c
le_antisymm
a ≤ b → b ≤ a → a = b
le_sup_left
a ≤ a ⊔ b
le_sup_right
b ≤ a ⊔ b
sup_le
a ≤ c → b ≤ c → a ⊔ b ≤ c
inf_le_left
a ⊓ b ≤ a
inf_le_right
a ⊓ b ≤ b
le_inf
c ≤ a → c ≤ b → c ≤ a ⊓ b
le_sSup
a ∈ s → a ≤ sSup s
sSup_le
(∀ a ∈ s, a ≤ b) → sSup s ≤ b
The omega tactic is very useful for goals involving natural number and integer
inequalities. For more general ordered structures, gcongr can help with
monotonicity goals.
Now try it yourself. The exercise sheet 06-a-Orders.lean turns the material above into sorry-holes to fill in your editor -- partial orders, lattices, sSup/sInf, and monotone functions. To run it in the browser with no local install (the first launch builds Mathlib, so give it a minute), open the repository in Gitpod and go to that file.
Mathlib encodes the algebraic structures as a hierarchy of typeclasses. We
go through it from the ground up: basic operations, then groups, rings and
fields, then their morphisms and substructures.
Additive versus multiplicative. Mathlib systematically duplicates
algebraic notions into an additive (Add, +, 0, -a) and a
multiplicative (Mul, *, 1, a⁻¹) version. Lemmas for the
additive side are prefixed with add_: add_assoc, add_comm,
add_zero, neg_add_cancel, etc.
"Class" suffix.MulOneClass, AddZeroClass are the bare
algebraic laws for unit elements; they are weaker than a full monoid.
Bundled morphisms.→*, →+, →+* are bundled: they package a
function together with proofs that it preserves the relevant
operations. Lemmas are named map_mul, map_add, map_one,
map_zero, map_pow, map_neg.
Subgroups and ideals.Subgroup G, Subring R, Ideal R are
bundled sub-objects with membership a ∈ H, closed under the
relevant operations (H.mul_mem, H.inv_mem, H.add_mem, etc.).
At the bottom of the hierarchy sit the basic operation typeclasses:
Mul α provides a multiplication * on α.
Add α provides an addition + on α.
One α provides a distinguished element 1 : α.
Zero α provides a distinguished element 0 : α.
These are combined step by step:
MulOneClass α : a type with * and 1, satisfying 1 * a = a and a * 1 = a.
Monoid α : a MulOneClass with associativity of *.
AddMonoid α : the additive version (with + and 0).
CommMonoid α : a monoid where * is commutative.
-- ℕ is an additive commutative monoidinferInstance : AddCommMonoidℕ#check(inferInstance:AddCommMonoidℕ)-- Associativity is available as a lemmaexample(abc:ℕ):a+b+c=a+(b+c):=add_assocabc-- The identity elementexample(a:ℕ):a+0=a:=add_zeroa
-- ℤ is an additive commutative groupinferInstance : AddCommGroupℤ#check(inferInstance:AddCommGroupℤ)-- Cancellationexample(ab:ℤ):a+b-b=a:=bya:ℤb:ℤ⊢ a+b-b=aomegaAll goals completed! 🐙-- In a general groupexample{G:Type*}[GroupG](a:G):a*a⁻¹=1:=mul_inv_cancelaexample{G:Type*}[GroupG](ab:G):(a*b)⁻¹=b⁻¹*a⁻¹:=mul_inv_revab
A ring has both addition (forming an abelian group) and multiplication
(forming a monoid), linked by distributivity.
Ring α : a (unital) ring.
CommRing α : a commutative ring.
Field α : a field (commutative ring where every nonzero element is invertible).
-- ℝ is a field, hence also a commutative ringinferInstance : Fieldℝ#check(inferInstance:Fieldℝ)inferInstance : CommRingℝ#check(inferInstance:CommRingℝ)-- Distributivityexample(abc:ℝ):a*(b+c)=a*b+a*c:=mul_addabc-- Division in a fieldexample(a:ℝ)(ha:a≠0):a*a⁻¹=1:=mul_inv_cancel₀ha
Mathlib also provides ordered variants:
OrderedRing α : a ring with a compatible order.
LinearOrderedField α : a linearly ordered field (like ℝ or ℚ).
Algebraic morphisms are functions that preserve the relevant structure.
Mathlib uses bundled morphisms (structures carrying both the function and the
proof of preservation).
MonoidHom (notation →*) : maps preserving * and 1.
AddMonoidHom (notation →+) : maps preserving + and 0.
-- The inclusion ℤ → ℚ is a ring homomorphismInt.castRingHomℚ : ℤ→+*ℚ#check(Int.castRingHomℚ:ℤ→+*ℚ)-- A ring homomorphism preserves additionexample(f:ℤ→+*ℚ)(ab:ℤ):f(a+b)=fa+fb:=f.map_addab-- A ring homomorphism preserves multiplicationexample(f:ℤ→+*ℚ)(ab:ℤ):f(a*b)=fa*fb:=f.map_mulab-- A ring homomorphism maps 0 to 0example(f:ℤ→+*ℚ):f0=0:=f.map_zero
Mathlib provides types for substructures of algebraic objects:
Subgroup G : a subgroup of G.
Subring R : a subring of R.
Subalgebra R A : a subalgebra of A over R.
Ideal R : an ideal of a (commutative) ring R.
These are bundled: a Subgroup G consists of a carrier set together with
proofs that it is closed under the group operations.
-- The trivial subgroup (just the identity)example{G:Type*}[GroupG]:SubgroupG:=⊥-- The whole group as a subgroupexample{G:Type*}[GroupG]:SubgroupG:=⊤-- Membershipexample{G:Type*}[GroupG](H:SubgroupG)(ab:G)(ha:a∈H)(hb:b∈H):a*b∈H:=H.mul_memhahb-- Subgroups form a complete latticeinferInstance : CompleteLattice(AddSubgroupℤ)#check(inferInstance:CompleteLattice(AddSubgroupℤ))
The ext tactic is very useful when proving equalities between algebraic
objects. For instance, two subgroups are equal if they have the same elements;
two morphisms are equal if they agree on all inputs.
-- Two subgroups with the same carrier are equalexample{G:Type*}[GroupG](HK:SubgroupG)(h:∀g,g∈H↔g∈K):H=K:=byG:Type u_1inst✝:GroupGH:SubgroupGK:SubgroupGh:∀(g:G),g∈H↔g∈K⊢ H=KextgG:Type u_1inst✝:GroupGH:SubgroupGK:SubgroupGh:∀(g:G),g∈H↔g∈Kg:G⊢ g∈H↔g∈KexacthgAll goals completed! 🐙-- Two ring homomorphisms agreeing on all elements are equalexample{RS:Type*}[RingR][RingS](fg:R→+*S)(h:∀r,fr=gr):f=g:=byR:Type u_1S:Type u_2inst✝¹:RingRinst✝:RingSf:R→+*Sg:R→+*Sh:∀(r:R),fr=gr⊢ f=gextrR:Type u_1S:Type u_2inst✝¹:RingRinst✝:RingSf:R→+*Sg:R→+*Sh:∀(r:R),fr=grr:R⊢ fr=grexacthrAll goals completed! 🐙
When working with Mathlib's algebraic hierarchy, recall the
exploration commands introduced in the Lean chapter: #check for
types, #print for definitions and fields of typeclasses,
inferInstance for checking whether a given instance exists, and
exact? / apply? for lemma search. In addition:
Look at Mathlib.Algebra and Mathlib.GroupTheory for the files
relevant to the structure you are working with.
Lemma names follow the predictable pattern <operation>_<result>,
e.g. mul_assoc, add_comm, map_mul, inv_inv.
-- Check what instances ℝ hasinferInstance : Fieldℝ#check(inferInstance:Fieldℝ)inferInstance : LinearOrderℝ#check(inferInstance:LinearOrderℝ)inferInstance : AddCommGroupℝ#check(inferInstance:AddCommGroupℝ)-- A general proof that works for any commutative ringexample{R:Type*}[CommRingR](ab:R):(a+b)^2=a^2+2*a*b+b^2:=byR:Type u_1inst✝:CommRingRa:Rb:R⊢ (a+b)^2=a^2+2*a*b+b^2ringAll goals completed! 🐙
The ring tactic is a decision procedure that closes goals that are
identities in commutative (semi)rings. Similarly, group works for group
identities, and field_simp helps simplify field expressions involving
division.
Filters give limits, neighbourhoods, convergence and "eventually" a
single framework, in place of the several epsilon-delta definitions of
classical analysis.
The exposition below follows the pedagogical approach of Kevin Buzzard and
Bhavik Mehta in their
Formalising Mathematics notes:
a filter is a generalized subset.
A short slide deck accompanies this chapter: it
states the learning goal and the three axioms, and is meant for the
opening minutes of a lecture rather than as a substitute for the text
below.
F ≤ G is pronounced "F is finer than G": smaller filters contain
more sets. The slogan is: the smaller the filter, the bigger its
collection of sets.
The prefix mem_ (as in mem_principal, mem_atTop_sets,
mem_nhds_iff) names lemmas that characterize membership in a
given filter.
𝓟 and 𝓝 are mathematical script letters, typed \McP and
\nhds; they are notation, not definitions.
⁻¹' is the preimage of a set under a function (not to be confused
with the inverse ⁻¹ of a group element); it is typed \inv' (or
\i\n\v ' in sequence).
Morally, a filter on a type α is a generalized subset of α. Every
ordinary subset gives rise to a filter, but there are other filters
representing "ideas" that cannot be described by a single subset, such as
an infinitesimal neighborhood of a point in a topological space, or a
neighborhood of ∞ in a totally ordered set. Isaac Newton wanted dx
to be "a real number infinitesimally close to 0"; filters are a modern
way to recover such thoughts.
The property we want a generalized subset F to have is that it is
uniquely determined by the actual subsets which contain F. If F is
a generalized subset of α, what properties should the collection of
actual subsets containing F have?
If S contains F and S ⊆ T, then T contains F.
If S and T both contain F, then so does S ∩ T.
The whole type Set.univ : Set α contains F.
These three axioms turn out to be enough: a filter is modelled precisely
as a collection of subsets of α satisfying them.
A filter F on a type α is a collection of sets of α which contains the whole α,
is upwards-closed, and is stable under intersection. We do not forbid this collection to be
all sets of α.
If two sets belong to a filter, then their intersection belongs to the filter as well.
Some authors add a fourth axiom forbidding ∅ ∈ sets. Mathlib does not:
this is analogous to disallowing a ring as an ideal of itself. It makes
the initial definition cleaner but introduces many awkward edge cases
later, so Mathlib keeps the "empty filter" ⊥ as a legitimate object.
If F : Filter α and S : Set α, the notation S ∈ F is sugar for
S ∈ F.sets. Read it as F ⊆ S. That expression does not type-check, because F is
a filter and not a set.
The axioms are restated in the Filter namespace under friendlier
names, rendered here directly from Mathlib:
Filters form a partial order by ≤. The definition is
F ≤ G ↔ G.sets ⊆ F.sets
which looks reversed, but matches the "generalized subset" intuition: if
F is contained in G as generalized subsets, then every actual subset
containing G also contains F, so G.sets ⊆ F.sets. A useful slogan:
The simplest filter is the principal filter, with notation 𝓟 s.
It is the filter whose sets are exactly the supersets of s -- i.e.
the generalized subset corresponding to the ordinary subset s.
The principal filter of s is the collection of all supersets of s.
-- 𝓟 s contains exactly the supersets of sexample{α:Type*}(st:Setα):t∈Filter.principals↔s⊆t:=Filter.mem_principal-- Every s belongs to its own principal filterexample{α:Type*}(s:Setα):s∈Filter.principals:=Filter.mem_principal_selfs-- The principal filter on the whole space is ⊤example{α:Type*}:Filter.principal(Set.univ:Setα)=⊤:=Filter.principal_univ-- The order on principal filters matches subset inclusionexample{α:Type*}(st:Setα):Filter.principals≤Filter.principalt↔s⊆t:=Filter.principal_mono
On any preorder, Filter.atTop is the filter of sets that contain all
sufficiently large elements. Morally, atTop is a "neighborhood of
∞": it represents "arbitrarily large elements" even though no single
largest element exists.
atTop is the filter representing the limit → ∞ on an ordered set.
It is generated by the collection of up-sets {b|a≤b}.
(The preorder need not have a top element for this to be well defined,
and indeed is trivial when a top element x exists, i.e., it coincides with purex.)
-- Characterization of membership in atTopexample(s:Setℕ):s∈Filter.atTop↔∃a,∀b,a≤b→b∈s:=Filter.mem_atTop_setsexample:{n:ℕ|n≥100}∈(Filter.atTop:Filterℕ):=by⊢ {n|n≥100}∈Filter.atToprw[Filter.mem_atTop_sets⊢ ∃a,∀(b:ℕ),a≤b→b∈{n|n≥100}]⊢ ∃a,∀(b:ℕ),a≤b→b∈{n|n≥100}exact⟨100,funbhb↦hb⟩All goals completed! 🐙
This filter is a generalized subset of α representing "almost every
element". On ℕ it coincides with atTop (the lemma
Nat.cofinite_eq_atTop): a set omits only finitely many naturals
exactly when it contains every sufficiently large one. On other types
the two differ, and comparing filters like these is a good way to train
the "generalized subset" intuition.
Given F : Filter α and m : α → β, there is a natural way to build a
filter on β: a set s : Set β is in the pushforward if its preimage
is in F. This is the filter-theoretic counterpart of the image of a
subset.
Filter.Tendsto is the generic "limit of a function" predicate.
Tendstofl₁l₂ asserts that for every l₂ neighborhood a,
the f-preimage of a is an l₁ neighborhood.
Unpacking: Tendsto f l₁ l₂ says that for every s ∈ l₂, the preimage
f ⁻¹' s is in l₁. In the generalized-subset picture, it says that
f sends the generalized subset l₁ into the generalized subset l₂.
This single definition unifies all classical limits:
Classical statement
Filter version
aₙ → l as n → ∞
Tendsto a atTop (nhds l)
f(x) → l as x → x₀
Tendsto f (nhds x₀) (nhds l)
f(x) → l as x → ∞
Tendsto f atTop (nhds l)
f(x) → ∞ as x → ∞
Tendsto f atTop atTop
-- Sanity check: Tendsto for sequences-- is the epsilon-N definitionexample(a:ℕ→ℝ)(l:ℝ):Filter.TendstoaFilter.atTop(nhdsl)↔∀ε>0,∃N,∀n≥N,|an-l|<ε:=bya:ℕ→ℝl:ℝ⊢ Filter.TendstoaFilter.atTop(nhdsl)↔∀ε>0,∃N,∀n≥N,|an-l|<εrw[Metric.tendsto_atTopa:ℕ→ℝl:ℝ⊢ (∀ε>0,∃N,∀n≥N,dist(an)l<ε)↔∀ε>0,∃N,∀n≥N,|an-l|<ε]a:ℕ→ℝl:ℝ⊢ (∀ε>0,∃N,∀n≥N,dist(an)l<ε)↔∀ε>0,∃N,∀n≥N,|an-l|<εsimp[Real.dist_eq]All goals completed! 🐙
Composition: if a → l and f is continuous at l, then f ∘ a → f l:
Filters let us speak precisely about something being true "eventually".
Filter.Eventually p F, with notation ∀ᶠ x in F, p x, is defined as
{x | p x} ∈ F. Read it as: "p holds for generalized-almost-all x
with respect to F."
For F = atTop on ℕ, ∀ᶠ n in atTop, p n means "for all sufficiently large n, p n".
For F = nhds x₀, ∀ᶠ x in nhds x₀, p x means "p holds on a neighborhood of x₀".
For F = cofinite, it means "p holds off a finite set".
The dual Filter.Frequently p F (notation ∃ᶠ x in F, p x) is the
negation of Eventually (¬ p) and means "p holds on an F-non-negligible
set".
-- Eventually for atTop unfolds to an existentialexample{p:ℕ→Prop}:(∀ᶠninFilter.atTop,pn)↔∃N,∀n≥N,pn:=Filter.eventually_atTop-- Finite conjunctions of eventually hold eventuallyexample{α:Type*}{F:Filterα}{pq:α→Prop}(hp:∀ᶠxinF,px)(hq:∀ᶠxinF,qx):∀ᶠxinF,px∧qx:=hp.andhq-- Tendsto preserves Eventually@Filter.Tendsto.eventually : ∀{α:Type u_1}{β:Type u_2}{f:α→β}{l₁:Filterα}{l₂:Filterβ}{p:β→Prop},Filter.Tendstofl₁l₂→(∀ᶠ(y:β)inl₂,py)→∀ᶠ(x:α)inl₁,p(fx)#check@Filter.Tendsto.eventually
When you have several ∀ᶠ-hypotheses and want to conclude another
∀ᶠ-statement pointwise, the dedicated tactic
filter_upwards [h₁, h₂, …] with x h₁ h₂ … intersects them and
hands you the pointwise goal:
A remark on ≥. Lean generally prefers ≤ over ≥, and most lemmas
are stated with ≤. The binder ∀ n ≥ N, ... is a special exception
(because ∀ N ≤ n, ... would bind N rather than n), but in bare
inequalities prefer to write 1 < n over n > 1.
To see all of the pieces working together, let us show that
1 / (n + 1) → 0 as n → ∞, using Tendsto, atTop, and nhds 0
in the metric setting:
example:Filter.Tendsto(funn:ℕ=>1/((n:ℝ)+1))Filter.atTop(nhds0):=by⊢ Filter.Tendsto(funn=>1/(↑n+1))Filter.atTop(nhds0)rw[Metric.tendsto_atTop⊢ ∀ε>0,∃N,∀n≥N,dist(1/(↑n+1))0<ε]⊢ ∀ε>0,∃N,∀n≥N,dist(1/(↑n+1))0<εintroεhεε:ℝhε:ε>0⊢ ∃N,∀n≥N,dist(1/(↑n+1))0<ε-- Find N with 1/(N+1) < ε:-- it suffices that N + 1 > 1/ε.obtain⟨N,hN⟩:=exists_nat_gt(1/ε)ε:ℝhε:ε>0N:ℕhN:1/ε<↑N⊢ ∃N,∀n≥N,dist(1/(↑n+1))0<εrefine⟨N,funnhn=>?_⟩ε:ℝhε:ε>0N:ℕhN:1/ε<↑Nn:ℕhn:n≥N⊢ dist(1/(↑n+1))0<εhavehε':(0:ℝ)<(n:ℝ)+1:=by⊢ Filter.Tendsto(funn=>1/(↑n+1))Filter.atTop(nhds0)positivityε:ℝhε:ε>0N:ℕhN:1/ε<↑Nn:ℕhn:n≥Nhε':0<↑n+1⊢ dist(1/(↑n+1))0<εhavehN':(1:ℝ)/ε<(n:ℝ)+1:=by⊢ Filter.Tendsto(funn=>1/(↑n+1))Filter.atTop(nhds0)calc(1:ℝ)/ε<N:=hN_≤n:=byε:ℝhε:ε>0N:ℕhN:1/ε<↑Nn:ℕhn:n≥Nhε':0<↑n+1⊢ ↑N≤↑nexact_mod_casthnAll goals completed! 🐙_<(n:ℝ)+1:=byε:ℝhε:ε>0N:ℕhN:1/ε<↑Nn:ℕhn:n≥Nhε':0<↑n+1⊢ ↑n<↑n+1linarithε:ℝhε:ε>0N:ℕhN:1/ε<↑Nn:ℕhn:n≥Nhε':0<↑n+1hN':1/ε<↑n+1⊢ dist(1/(↑n+1))0<εrw[Real.dist_eq,ε:ℝhε:ε>0N:ℕhN:1/ε<↑Nn:ℕhn:n≥Nhε':0<↑n+1hN':1/ε<↑n+1⊢ |1/(↑n+1)-0|<εsub_zero,ε:ℝhε:ε>0N:ℕhN:1/ε<↑Nn:ℕhn:n≥Nhε':0<↑n+1hN':1/ε<↑n+1⊢ |1/(↑n+1)|<εabs_of_pos(byε:ℝhε:ε>0N:ℕhN:1/ε<↑Nn:ℕhn:n≥Nhε':0<↑n+1hN':1/ε<↑n+1⊢ 0<1/(↑n+1)positivityAll goals completed! 🐙)]ε:ℝhε:ε>0N:ℕhN:1/ε<↑Nn:ℕhn:n≥Nhε':0<↑n+1hN':1/ε<↑n+1⊢ 1/(↑n+1)<εrw[div_lt_iff₀hε'ε:ℝhε:ε>0N:ℕhN:1/ε<↑Nn:ℕhn:n≥Nhε':0<↑n+1hN':1/ε<↑n+1⊢ 1<ε*(↑n+1)]ε:ℝhε:ε>0N:ℕhN:1/ε<↑Nn:ℕhn:n≥Nhε':0<↑n+1hN':1/ε<↑n+1⊢ 1<ε*(↑n+1)have:1<ε*((n:ℝ)+1):=by⊢ Filter.Tendsto(funn=>1/(↑n+1))Filter.atTop(nhds0)haveh:=(div_lt_iff₀hε).mphN'ε:ℝhε:ε>0N:ℕhN:1/ε<↑Nn:ℕhn:n≥Nhε':0<↑n+1hN':1/ε<↑n+1h:1<(↑n+1)*ε⊢ 1<ε*(↑n+1)linarith[h]ε:ℝhε:ε>0N:ℕhN:1/ε<↑Nn:ℕhn:n≥Nhε':0<↑n+1hN':1/ε<↑n+1this:1<ε*(↑n+1)⊢ 1<ε*(↑n+1)linarithAll goals completed! 🐙
The structure of the proof mirrors the classical epsilon-N argument
exactly: pick N, then show n ≥ N → |1 / (n+1) - 0| < ε. The
filter language lets us state convergence in one line; the actual
work is still the inequality manipulation.
An ultrafilter is a maximal proper filter (xxx: proper filter is not defined): for every set s, either
s or sᶜ belongs to the ultrafilter. Ultrafilters are fundamental in
logic (they correspond to consistent complete theories) and in topology
(compactifications, nonstandard analysis).
Topology in Mathlib is built on the filter framework. The primary
definition of continuity is Filter.Tendsto on neighbourhood filters,
not epsilon-delta and not open preimages. Topological spaces, continuity and
metric spaces all follow from that choice.
A topological space in Mathlib is a typeclass TopologicalSpace α that
specifies which sets are open. However, the actual definition in Mathlib
uses the neighborhood filter approach: a TopologicalSpace is given by
specifying, for each point x, its neighborhood filter nhds x.
Equivalently, one can define it via IsOpen. Here is the structure
as it appears in Mathlib:
-- The empty set and the whole space are openexample{α:Type*}[TopologicalSpaceα]:IsOpen(∅:Setα):=isOpen_emptyexample{α:Type*}[TopologicalSpaceα]:IsOpen(Set.univ:Setα):=isOpen_univ-- A set is closed iff its complement is openexample{α:Type*}[TopologicalSpaceα](s:Setα):IsCloseds↔IsOpensᶜ:=isOpen_compl_iff.symm
Finite intersections and arbitrary unions of open sets are open:
-- A set is closed iff it equals its closure.example{α:Type*}[TopologicalSpaceα](s:Setα):IsCloseds↔closures=s:=⟨funh=>h.closure_eq,funh=>h▸isClosed_closure⟩-- The interior of a set is open.example{α:Type*}[TopologicalSpaceα](s:Setα):IsOpen(interiors):=isOpen_interior
For a point x in a topological space, nhds x is the filter of
neighborhoods of x. A set s is a neighborhood of x if there is an
open set U with x ∈ U ⊆ s.
A set is called a neighborhood of x if it contains an open set around x. The set of all
neighborhoods of x forms a filter, the neighborhood filter at x, is here defined as the
infimum over the principal filters of all open sets containing x.
A function between topological spaces is continuous if the preimage
of every open set is open. Registered as a structure to make sure it is not unfolded by Lean.
A set s is compact if every open cover has a finite subcover. In Mathlib,
compactness is defined using filters: s is compact if every filter that
contains s (via the principal filter) and is nontrivial has a cluster point
in s. (xxx: nontrivial filter and cluster point are used here but not defined.)
A topological space is connected if it cannot be written as the union of
two nonempty disjoint open sets. A set s is connected if, viewed as a
subspace, it is connected. (xxx: the distinction connected vs preconnected -- used in the notation table and summary -- is not explained.)
-- ℝ is connectedinferInstance : ConnectedSpaceℝ#check(inferInstance:ConnectedSpaceℝ)
A MetricSpace α provides a distance function dist : α → α → ℝ satisfying
the usual axioms. Every metric space is automatically a topological space
(the topology is induced by the metric).
-- ℝ is a metric spaceinferInstance : MetricSpaceℝ#check(inferInstance:MetricSpaceℝ)-- The distance function@dist : {α:Type u_1}→[self:Distα]→α→α→ℝ#check@dist-- dist : α → α → ℝ-- Key propertiesexample(xy:ℝ):distxy≥0:=dist_nonnegexample(xy:ℝ):distxy=distyx:=dist_commxy-- Triangle inequalityexample(xyz:ℝ):distxz≤distxy+distyz:=dist_trianglexyz-- dist x y = 0 iff x = yexample(xy:ℝ):distxy=0↔x=y:=dist_eq_zero
In a metric space, nhds x is generated by open balls:
-- Example: the function x ↦ 2 * x is continuous on ℝexample:Continuous(funx:ℝ↦2*x):=by⊢ Continuousfunx=>2*xexactcontinuous_const.mulcontinuous_idAll goals completed! 🐙
Given topological spaces α and β, the product α × β carries the
product topology, where a basis of open sets is given by products of open
sets.
-- The product of topological spaces is a topological spaceinferInstance : TopologicalSpace(ℝ×ℝ)#check(inferInstance:TopologicalSpace(ℝ×ℝ))-- Projections are continuousexample:Continuous(Prod.fst:ℝ×ℝ→ℝ):=continuous_fstexample:Continuous(Prod.snd:ℝ×ℝ→ℝ):=continuous_snd
A function into a product is continuous iff both components are:
The key insight is that all of these interact seamlessly with the filter
framework from the previous section. Limits, continuity, and compactness
are all expressed in terms of filters.
Measure theory in Mathlib covers sigma-algebras, measurable functions,
measures, integration and probability, and uses the filter framework for
the convergence theorems.
Prefix Is for properties of measures: IsProbabilityMeasure μ,
IsFiniteMeasure μ, IsSigmaFiniteMeasure μ.
Prefix ae (short for "almost everywhere"): ae μ is the filter
of conull sets; AEMeasurable f, AEStronglyMeasurable f,
AEEqFun are ae-variants of stronger regularity properties.
The integral symbol ∫ takes a binder ∫ x, ... followed by the
measure ∂μ. Without ∂μ, the integral is with respect to the
default volume measure.
ℝ≥0∞ is the preferred codomain for measures: they take values in
[0, ∞]. Coercions to/from ℝ use ENNReal.toReal,
ENNReal.ofReal.
Almost-everywhere statements are just the filter quantifier ∀ᶠ
applied to the ae filter: ∀ᵐ x ∂μ, p x is definitionally
∀ᶠ x in μ.ae, p x.
A sigma-algebra (or measurable space) on a type α is specified by the
typeclass MeasurableSpace α. It tells Lean which subsets of α are
measurable. (xxx: the σ-algebra axioms -- contains ∅, closed under complement and countable union -- are not stated in prose.)
The union of a sequence of measurable sets is a measurable set. Use a more general
MeasurableSet.iUnion instead.
-- ℝ has a measurable space structure (the Borel sigma-algebra)inferInstance : MeasurableSpaceℝ#check(inferInstance:MeasurableSpaceℝ)-- The whole space and the empty set are measurableexample:MeasurableSet(Set.univ:Setℝ):=MeasurableSet.univexample:MeasurableSet(∅:Setℝ):=MeasurableSet.empty-- Complements of measurable sets are measurableexample(s:Setℝ)(hs:MeasurableSets):MeasurableSetsᶜ:=hs.compl
Countable unions of measurable sets are measurable:
A measure on a measurable space α is a countably additive function from
measurable sets to [0, ∞]. In Mathlib, measures take values in ℝ≥0∞
(the extended nonnegative reals, typed \R\ge0\infty or using ENNReal).
A measure is defined to be an outer measure that is countably additive on
measurable sets, with the additional assumption that the outer measure is the canonical
extension of the restricted measure.
The measure of a set s, denoted μs, is an extended nonnegative real. The real-valued version
is written μ.reals.
Mathlib also gives the volume of Ico, Ioo, Ioc (all equal to
b - a when a ≤ b); a half-open interval can be written as a
disjoint union with a single point, and points have measure zero
(Real.volume_singleton).
Mathlib provides the Bochner integral ∫ x, f x ∂μ (typed \int), which
generalizes both the Lebesgue integral for real-valued functions and the
integral for Banach-space-valued functions. (xxx: the construction of the integral -- simple-function approximation, lintegral -- and SigmaFinite are named but never sketched.)
The Bochner integral is linear. Note this requires 𝕜 to be a normed division ring, in order
to ensure that for c≠0, the function c•f is integrable iff f is. For an analogous
statement for more general rings with an a priori integrability assumption on f, see
MeasureTheory.Integrable.integral_smul.
The fundamental convergence theorems of measure theory are available in Mathlib:
Monotone Convergence Theorem: If f₀ ≤ f₁ ≤ f₂ ≤ ... is an increasing
sequence of nonnegative measurable functions, then
∫⁻ x, (⨆ n, f n x) ∂μ = ⨆ n, ∫⁻ x, f n x ∂μ.
Lebesgue dominated convergence theorem provides sufficient conditions under which almost
everywhere convergence of a sequence of functions implies the convergence of their integrals.
We could weaken the condition bound_integrable to require HasFiniteIntegralboundμ instead
(i.e. not requiring that bound is measurable), but in all applications proving integrability
is easier.
These theorems are stated using filters: convergence of the integrals is
expressed as Filter.Tendsto with respect to Filter.atTop.
Many statements in measure theory hold "almost everywhere" (a.e.), meaning
outside a set of measure zero. In Mathlib, this is captured by the
ae-filterMeasureTheory.Measure.ae μ, which is the filter of sets whose
complement has measure zero.
-- "f = g almost everywhere" means {x | f x ≠ g x} has measure zeroopenMeasureTheoryinexample{α:Type*}[MeasurableSpaceα](μ:Measureα)(fg:α→ℝ):f=ᵐ[μ]g↔∀ᶠxinaeμ,fx=gx:=Iff.rfl
"Almost everywhere" is "eventually" with respect to the ae-filter.
For probability theory, Mathlib provides conditional expectation with respect
to a sub-sigma-algebra. This is an advanced topic, but the key definition is:
The conditional expectation μ[f | m] (notation
MeasureTheory.condExp m μ f) is the unique (up to
μ-almost-everywhere equality) function g : α → E such that
g is m-measurable, where m is a sub-sigma-algebra of the
ambient sigma-algebra on α;
for every m-measurable set s,
∫ x in s, f x ∂μ = ∫ x in s, g x ∂μ.
In probability terms: μ[f | m] is the best m-measurable
predictor of f in the L² sense. The defining property is
captured by MeasureTheory.setIntegral_condExp. Mathlib has the
linearity, monotonicity, tower, and Jensen properties; the API
lives in the MeasureTheory namespace and uses condExp_ as a
prefix.
The conditional expectation μ[f | m] is the unique (a.e.) m-measurable
function that agrees with f in integral over every m-measurable set.
2.8.12. Summary of key Mathlib API for measure theory🔗
Concept
Mathlib name
Sigma-algebra
MeasurableSpace α
Measurable set
MeasurableSet s
Measurable function
Measurable f
Measure
MeasureTheory.Measure α
Lebesgue measure
MeasureTheory.volume
Bochner integral
∫ x, f x ∂μ
Lebesgue integral
∫⁻ x, f x ∂μ
Probability measure
IsProbabilityMeasure μ
Almost everywhere
∀ᶠ x in μ.ae, p x
Integrable
MeasureTheory.Integrable f μ
Conditional expectation
MeasureTheory.condExp
Working with measure theory in Mathlib requires familiarity with ℝ≥0∞
(extended nonnegative reals) and the filter framework. Many lemmas require
measurability or integrability hypotheses, so expect to prove these as
side goals.
First, monads: a design pattern from functional programming that
sequences computations carrying additional structure -- failure,
multiple results, state, I/O, probability. Second, Mathlib's PMF α,
the type of discrete probability distributions on α, which is a monad
in exactly that sense.
Consider looking up values in a list of key-value pairs. The lookup might fail if the key is not present. Lean represents this with the Option type:
Option : Type u_1→Type u_1#check@Option-- Option : Type u → Type u
A value of type Option α is either some a (for some a : α) or none. This is like a computation that might produce a result or might fail.
defsafeDivide(ab:ℕ):Optionℕ:=ifb==0thennoneelsesome(a/b)some3#evalsafeDivide103-- outputs some 3none#evalsafeDivide100-- outputs none
Now suppose we want to chain several such computations: divide a by b, then divide the result by c. Without special support, we would need to write nested pattern matches:
The let r ← e syntax means: "run the computation e; if it succeeds with value r, continue; if it fails, propagate the failure." Under the hood, Lean translates this into >>= calls.
Before we discuss monads, we need the simpler concept of a functor. In Lean, Functor is a typeclass for type constructors f : Type → Type that support a map operation:
map : (α → β) → f α → f β -- applies a function to the value(s) inside the container, without changing the structure.
The infix notation for map is <$>, so g <$> x means Functor.map g x. Every monad is automatically a functor (since map can be defined in terms of bind and pure), and the functor interface suffices when you only transform values without chaining computations.
Here is what map does for each of our examples:
Option: map f x applies f to the contained value if x = some a, giving some (f a), and returns none if x = none.
some15#eval(·+10)<$>(some5:Optionℕ)-- some 15none#eval(·+10)<$>(none:Optionℕ)-- none
List: map f xs applies f to every element of the list, i.e. List.map f xs.
[2,4,6]#eval(·*2)<$>[1,2,3]-- [2, 4, 6]
IO: map f action produces a new IO action that runs action and then applies f to its result.
StateM σ: map f x runs the stateful computation x, obtains result a and new state s', and returns (f a, s'). The state is threaded through unchanged.
Set: map f s is the image of s under f, i.e. {f a | a ∈ s}. This is the familiar image operation from mathematics.
Filter: Filter.map f F is the pushforward filter. A set s belongs to Filter.map f F if and only if f ⁻¹' s ∈ F. This generalizes the image operation to filters.
Finset: map f s applies an embedding f to each element of the finite set. For a plain function, one uses Finset.image f s instead.
In summary, map always means "apply a function inside the container." The key property is that map preserves the structure: it cannot change none to some, shorten a list, or introduce new side effects. It only transforms the values.
In Lean, Monad is a typeclass. Any type constructor m : Type → Type can be a monad if it provides two operations:
pure : α → m α -- wraps a plain value into the monadic context.
bind : m α → (α → m β) → m β -- chains computations.
These must satisfy the monad laws (associativity and identity), which ensure that do-notation behaves sensibly. Every Monad is also a Functor, with map f x = x >>= (pure ∘ f).
Lean already provides Monad Option, Monad List, Monad IO, and many more. For each monad we encounter below, we will spell out what pure and bind mean concretely.
Here, let x ← [1, 2, 3] means "for each x in the list [1, 2, 3]," and return (x, y) creates a singleton list. The monadic bind for lists is essentially List.flatMap (also known as concatMap or List.bind).
This is similar to set-builder notation in mathematics: { (x, y) | x ∈ {1,2,3}, y ∈ {10,20} }.
The monad instance for List is given by:
pure (a : α) : List α := [a]
bind (xs : List α) (f : α → List β) : List β := xs.flatMap f
So pure creates a singleton list, and bind applies f to each element and concatenates the resulting lists. For example, [1, 2].bind (fun x => [x, x * 10]) evaluates to [1, 10, 2, 20].
Functional programming insists on pure functions, while real programs read files, print to the screen and talk over a network. An IO α computation describes an action that, when executed, may perform side effects and produce a value of type α.
defgreet(name:String):IOUnit:=doIO.printlns!"Hello, {name}!"IO.println"Welcome to Lean."
Inside do-notation, we can sequence IO actions. The key insight is that greet itself is a pure function -- it returns a description of actions, not the actions themselves. The actions are only performed when the program runs.
A complete program in Lean is a value of type IO Unit:
defmyMain:IOUnit:=doIO.println"What is 2 + 2?"IO.printlns!"It is {2+2}."
The monad instance for IO is given by:
pure (a : α) : IO α -- returns a without performing any side effect.
bind (x : IO α) (f : α → IO β) : IO β -- first executes the action x, obtaining a value a : α, then executes f a.
Internally, IO α is defined as EStateM IO.Error IO.RealWorld α, a state monad over a token representing the real world. However, these implementation details are hidden: you cannot pattern-match on an IO value.
The StateM monad allows computations that read and modify state. A value of type StateM σ α is a computation that can access and modify state of type σ and produces a result of type α.
The .run function executes the stateful computation with an initial state. The result ((0, 1, 2), 3) tells us the three counter values were 0, 1, 2, and the final state is 3.
The type StateM σ α is defined as σ → (α × σ), i.e. a function that takes an initial state and returns a result together with an updated state. The monad instance is:
pure (a : α) : StateM σ α := fun s => (a, s) -- returns a and leaves the state unchanged.
bind (x : StateM σ α) (f : α → StateM σ β) : StateM σ β := fun s => let (a, s') := x s; f a s' -- runs x with state s, obtaining result a and new state s', then runs f a with state s'.
The operations get : StateM σ σ := fun s => (s, s) and set (s' : σ) : StateM σ Unit := fun _ => ((), s') allow reading and writing the state.
The Except type is like Option, but it carries an error message when something goes wrong:
defsafeDivideE(ab:ℕ):ExceptStringℕ:=ifb==0thenExcept.error"division by zero"elseExcept.ok(a/b)defcomputation:ExceptStringℕ:=doletr1←safeDivideE1005letr2←safeDivideEr14returnr2Except.ok 5#evalcomputation-- outputs Except.ok 5deffailingComputation:ExceptStringℕ:=doletr1←safeDivideE1000letr2←safeDivideEr14returnr2Except.error "division by zero"#evalfailingComputation-- outputs Except.error "division by zero"
With Except ε α, we can propagate meaningful error messages instead of just none.
The type Except ε α has two constructors: Except.ok (a : α) and Except.error (e : ε). The monad instance is:
pure (a : α) : Except ε α := Except.ok a
bind (x : Except ε α) (f : α → Except ε β) : Except ε β := match x with | Except.error e => Except.error e | Except.ok a => f a
This is exactly analogous to Option, but the failure case carries an error value of type ε instead of just being none.
In mathematics, a natural analogue of the List monad is the Set monad. A set s : Set α represents a nondeterministic choice among the elements of s. Mathlib provides a monad instance for Set, but it is not registered globally because sets have no computational content (you cannot iterate over an arbitrary Set). To use it, we enable it locally:
Recall that a filterF : Filter α is a collection of sets that is upward closed and closed under finite intersections. Filters generalize the notion of "sets of large elements" or "neighborhoods." Mathlib defines pure and bind operations for filters:
The Pure instance for Filter is registered so that pure a is the principal filter of {a}:
pure (a : α) : Filter α := 𝓟 {a} -- the collection of all sets containing a. In particular, s ∈ pure a ↔ a ∈ s.
Filter.bind (F : Filter α) (m : α → Filter β) : Filter β := Filter.join (Filter.map m F) -- informally, this takes the "union" of the filters m a as a ranges over F.
However, Mathlib does not register a Monad Filter instance. The reason is that this bind is incompatible with the Applicative Filter instance (which is based on Filter.seq), so the monad laws would not hold with respect to the existing applicative structure. One can still use pure and Filter.bind directly, but do-notation is not available for filters.
Finset α is the type of finite sets with elements in α. Unlike Set, finite sets do have computational content, and Mathlib registers a global Monad Finset instance.
Note that we need open scoped Classical because the Monad Finset instance requires all propositions to be decidable. This makes the definition noncomputable, so we cannot use #eval on it. However, we can still reason about it in proofs.
The monad instance for Finset is given by:
pure (a : α) : Finset α := {a} -- the singleton finite set.
bind (s : Finset α) (f : α → Finset β) : Finset β := s.sup f -- which equals s.biUnion f, the finite union of all images under f.
The name "monad" comes from category theory. In that setting, a monad on a category C is an endofunctor T : C → C together with natural transformations η : Id → T (the unit, corresponding to pure) and μ : T ∘ T → T (the multiplication, related to bind/join), satisfying certain coherence conditions.
The programming notion of monad is exactly this, specialized to the category of types and functions. The monad laws in programming:
pure a >>= f = f a -- left identity
m >>= pure = m -- right identity
(m >>= f) >>= g = m >>= (fun x => f x >>= g) -- associativity
correspond precisely to the coherence conditions for monads in category theory. So if you study category theory later, you will find that you already understand monads from a programming perspective.
Monads provide a unified pattern for sequencing computations:
Option -- computations that may fail silently
Except ε -- computations that may fail with an error of type ε
List -- computations with multiple results
Set -- nondeterministic choice over (possibly infinite) sets
Finset -- nondeterministic choice over finite sets
StateM σ -- computations with mutable state of type σ
IO -- computations with real-world side effects
Filters have pure and bind operations but do not form a Monad instance due to incompatibility with the applicative structure.
The do-notation makes monadic code readable and close to imperative style, while preserving the benefits of functional programming (purity, type safety, composability).
The next sections apply exactly this framework to discrete
probability distributions, packaged in Mathlib as PMF α.
Mathlib's PMF α models a discrete probability distribution on a
type α: it is a function α → ℝ≥0∞ whose values sum (as a
tsum) to 1. Unlike full measure theory, working with PMF lets
us avoid most measurability hypotheses: the support of a PMF is
automatically countable, and there is no sigma-algebra on α to
keep track of.
The goal of the rest of this chapter is to explain the monadic
structure of PMF: the four operations pure, bind, map,
join, and the laws they satisfy. This structure is exactly the
one introduced above, now applied to probability distributions.
A probability mass function, or discrete probability measures is a function α → ℝ≥0∞ such
that the values have (infinite) sum 1.
So a PMF α is a function p : α → ℝ≥0∞ together with a proof
HasSum p 1. (xxx: unconditional summation tsum/HasSum -- the crux of PMF -- is not defined.) In particular:
p a : ℝ≥0∞ is the probability mass at a;
∑' a, p a = 1 (total mass is one);
p.support = {a | p a ≠ 0} is automatically countable.
The countability of the support is the key reason we can largely
avoid measurability: a discrete distribution only "sees" countably
many values, so integrals against a PMF reduce to infinite sums.
Given p : PMF α and a family of distributions
f : α → PMF β, the composite distribution p.bind f : PMF β
draws a according to p, then draws b according to f a.
Its probability mass is
If p : PMF α and f : α → β, the image distributionp.map f : PMF β assigns to each b the total probability mass
that p places on the preimage f ⁻¹' {b}.
Notice how no measurability hypothesis appears: PMF.map f p makes
sense for every function f : α → β. Measure.map, by contrast, requires Measurable f and produces the
zero measure otherwise. Measurability only re-enters if we want
to compare p.map f with the pushforward measure p.toMeasure.map f,
as in PMF.toMeasure_map_apply -- but nothing on the PMF side
requires it.
A distribution of distributions is a PMF (PMF α): a random
choice of a random variable. The join operation collapses it
into a single PMF α by sampling at both stages.
Mathlib does not define PMF.join separately, because every monad
already supplies it as bind id. In Lean:
-- `join` derived from the monadic structurenoncomputabledefPMF.join{α:Type*}(P:PMF(PMFα)):PMFα:=P.bindid
Equivalently, unfolding bind,
(P.join) a = ∑' q, P q * q a.
The general monad identities give us
join (pure p) = p (from PMF.pure_bind);
join (p.map pure) = p (from PMF.bind_pure);
join (P.map (fun p ↦ p.map f)) = (join P).map f (naturality);
join (P.map join) = join (join P) (associativity of join),
all derivable from pure_bind, bind_pure, and bind_bind. No
measurability is used anywhere.
Mathlib provides several familiar named distributions as PMFs:
PMF.bernoulli p hp : PMF Bool -- the Bernoulli distribution with
probability p of true, requiring p ≤ 1.
PMF.binomial p hp n : PMF (Fin (n + 1)) -- the Binomial
distribution B(n, p).
PMF.uniformOfFinset s hs : PMF α -- the uniform distribution on
a nonempty finite set.
-- Fair coin: probability of true is 1/2.noncomputableexample:`PMF.bernoulli` has been deprecated: Use `ProbabilityTheory.bernoulliMeasure` insteadNote: The updated constant has a different type:{X:Type u_1}→[inst:MeasurableSpaceX]→X→X→↑unitInterval→MeasureTheory.MeasureXinstead of(p:NNReal)→p≤1→PMFBoolNote: The updated constant is in a different namespace. Dot notation may need to be changed (e.g., from `x.bernoulli` to `ProbabilityTheory.bernoulliMeasure x`).PMF.bernoulli(1/2)(by⊢ 1/2≤1norm_numAll goals completed! 🐙)true=1/2:=by⊢ (PMF.bernoulli(1/2)⋯)true=1/2simp[`PMF.bernoulli_apply` has been deprecated: Use `ProbabilityTheory.bernoulliMeasure_apply` insteadNote: The updated constant has a different type:∀{X:Type u_1}[inst:MeasurableSpaceX]{xy:X}(p:↑unitInterval){s:SetX},MeasurableSets→∀[inst_1:DecidablePredfunx=>x∈s],(ProbabilityTheory.bernoulliMeasurexyp)s=ifx∈sthenify∈sthen1else↑(unitInterval.toNNRealp)elseify∈sthen↑(unitInterval.toNNReal(unitInterval.symmp))else0instead of∀{p:NNReal}(h:p≤1)(b:Bool),(PMF.bernoulliph)b=↑(bifbthenpelse1-p)Note: The updated constant is in a different namespace. Dot notation may need to be changed (e.g., from `x.bernoulli_apply` to `ProbabilityTheory.bernoulliMeasure_apply x`).PMF.bernoulli_apply]All goals completed! 🐙
PMF is the right tool for discrete distributions, but probability
theory in Mathlib is much larger. A sketch of where to go next:
Every PMF α induces a probability measure on α via
PMF.toMeasure; conversely, Measure α is the general setting
that handles continuous distributions.
Expectation of a function f : α → ℝ against a probability
measure μ is the Bochner integral∫ x, f x ∂μ, found in
Mathlib.MeasureTheory.Integral.Bochner.
Independence of two events / sigma-algebras / random variables
is ProbabilityTheory.IndepSets, IndepFun, etc., in
Mathlib.Probability.Independence.
Conditional probability and conditional expectation live in
Mathlib.Probability.ConditionalProbability and the measure-theory
chapter of these notes.
The PMF monad goes a remarkably long way with no mention of
sigma-algebras. You only need to reach for measurability when:
You want to convert a PMF α to a Mathlib Measure α: this
uses PMF.toMeasure, which does require a MeasurableSpace α.
You want to integrate a function g : α → ℝ against a PMF and
reuse the Bochner-integral library. For discrete sums this is
usually avoidable; the PMF-level API (e.g. PMF.bind_apply)
gives you an explicit tsum.
You want to compare PMF.map f with the measure-theoretic
pushforward Measure.map f.
For most manipulations of discrete distributions -- composing
samplers, pushing them along functions, computing marginals --
working directly with PMF and its monad laws is cleaner and
measurability-free.