The toolkit that makes formalizing real mathematics tractable — dependent types, typeclasses, Mathlib search, and the tactic zoo.
dr Bartosz Naskręcki · Adam Mickiewicz University in Poznań · CCAI, Warsaw Univ. of Technology
▷ live lab: /lab-lambda
From one honest proof to a research paper
Lecture 4 got us a first kernel-checked proof: Nat, induction, a starter kit of tactics. Enough to reprove undergraduate lemmas one keystroke at a time — not enough to do mathematics at scale.
Two things stand in the way:
Generality — state a * b = b * aonce, at the right level of abstraction, and have it fire on $\N$, $\Z$, polynomials, matrices.
Automation & search — no human recalls a library this size. The skill is finding lemmas and letting decision procedures discharge the routine algebra.
The scale of the problem
~283,000
theorems in Mathlib
~135,000
definitions
2,000,000+
lines of formal mathematics
Mathlib is a single continuously-integrated monorepo — the largest formal-mathematics library in existence. You will never recall it; you decode and search it.
This lecture is that toolkit — and the on-ramp to Lecture 6, where the same tools carry a whole paper.
Run it yourself
Before dependent types, watch the untyped core compute — a logical connective reducing by β-reduction, proofs-as-programs made literal. Open the Lambda Lab and type:
reduce IMPLIES TRUE FALSE
ch term \p.\q. p
Algorithm W answers a principal type of the shape α → β → α — read the silent ∀ α β in front: the principal type is a Π-type with implicit binders. Lean just writes the binders out.
In Lean the same idea has types attached — #check (fun n : ℕ => rfl : ∀ n, n + 0 = n) type-checks because ∀is a Π-type.
Every calculation in this lecture is reproducible in the browser.
Dependent types: the one primitive
What separates Lean from a simply-typed language is the dependent function type (Π-type): given $\alpha$ and a family $\beta : \alpha \to \mathrm{Sort}\,u$,
The ordinary arrow $\alpha \to \beta$ is the degenerate case where $\beta$ ignores $x$. So $\forall$ and $\to$ are the same connective: a proof of ∀ n, P nis a function sending each n to a proof of P n. Types depend on values — Vector α n carries its length in its type.
Proof irrelevance. For p : Prop and proofs h₁ h₂ : p, they are definitionally equal: $h_1 \equiv h_2$. A proposition carries no data beyond being inhabited.
Large-elimination restriction. You cannot pattern-match a proof of ∃ n, P n to extract the witness as data — that would leak a non-computational choice through a proof-irrelevant door.
De Bruijn criterion: only the small kernel is trusted; #print axioms thm audits which axioms a proof really used.
ch type '((P -> Q) -> P) -> P'
Peirce's law comes back UNINHABITED — no λ-term proves it in intuitionistic STLC. That is exactly why classical principles enter Lean as axioms (Classical.choice), and why #print axioms has something to report.
Structures & typeclasses: the algebraic hierarchy
A structure bundles data with the proofs of its axioms. A class is a structure whose instances Lean finds automatically — the mechanism behind +, 0, ≤ meaning the right thing on every type at once.
class Group (G : Type*) extends DivInvMonoid G where
inv_mul_cancel : ∀ a : G, a⁻¹ * a = 1
State a theorem under [CommMonoid M] and it fires everywhere: $\texttt{mul\_comm} : a * b = b * a$ — proved once, inherited everywhere. The price is the diamond problem: two inheritance paths to a shared ancestor must give definitionally-equal instances.
Interface over definition
class MyAddMonoid (α : Type*) where
zero : α
add : α → α → α
add_zero : ∀ a, add a zero = a
zero_add : ∀ a, add zero a = a
instance : MyAddMonoid Nat where
zero := 0 ; add := Nat.add
add_zero := Nat.add_zero ; zero_add := Nat.zero_add
open MyAddMonoid in
theorem add_zero_zero {α : Type*} [MyAddMonoid α] (a : α) :
add (add a zero) zero = a := by
rw [add_zero, add_zero]
The proof never mentions Nat — it reasons from the interface, so it holds for every future instance for free.
How to search Mathlib
Decode names left-to-right: Finset.sum_range_succ = a sum over range (n+1), split at the last term.
Search in four modalities:
In-editor:exact?, apply?, rw?, simp? (pins a robust simp only [...]).
Loogle — syntactic: search by shape, e.g. "(_ * _) ^ _". Complete and predictable.
LeanSearch — neural/semantic: query in natural language when you don't know the shape.
Moogle — embedding-based, similar in spirit.
Reflex: exact? first, Loogle when you can describe the shape, LeanSearch when you can only describe the idea.
The tactic zoo — match the goal's shape
ring — commutative-(semi)ring identities; ignores hypotheses
linear_combination e — equality as a certificate of hypotheses
linarith / nlinarith — linear / nonlinear ordered arithmetic
omega — linear integer/nat arithmetic, complete
norm_num — closed numeric goals
field_simp — clear denominators, hand off to ring
positivity — prove $0 < e$, $0 \le e$, $e \neq 0$
decide — a Decidable prop, by kernel computation
New (Lean 4.22):grind, SMT-style, ships cutsat (supersedes omega) and a Gröbner solver (grobner). polyrith is retired — its Sage server was shut down.
Inequalities in one line: hand nlinarith the square
Cauchy–Schwarz, because the gap is a perfect square $(ay-bx)^2 \ge 0$:
theorem cauchy_schwarz (a b x y : ℝ) :
(a*x + b*y)^2 ≤ (a^2 + b^2) * (x^2 + y^2) := by
nlinarith [sq_nonneg (a*y - b*x)]
theorem amgm (a b : ℝ) (ha : 0 ≤ a) (hb : 0 ≤ b) :
2 * Real.sqrt (a*b) ≤ a + b := by
nlinarith [sq_nonneg (a - b), Real.sq_sqrt (mul_nonneg ha hb),
Real.sqrt_nonneg (a*b)]
Contrast a purely finite goal, closed by evaluation — Wilson at $p=7$: example : Nat.factorial 6 % 7 = 6 := by decide.
calc — proofs that read like the blackboard
Given $a^2 = b^2 + 4$ and $a + b = 2$, show $a - b = 2$. The prose survives intact:
example (a b : ℝ) (h1 : a^2 = b^2 + 4) (h2 : a + b = 2) : a - b = 2 := by
have hprod : (a - b) * (a + b) = 4 := by
calc (a - b) * (a + b) = a^2 - b^2 := by ring
_ = 4 := by rw [h1]; ring
rw [h2] at hprod -- hprod : (a - b) * 2 = 4
linarith
Through the Trans typeclass a single calc may mix =, ≤, <, ∣, ⊆. This legibility is what convinces mathematicians the formal text is honest.
Recursion: structural vs. well-founded
Structural
Every recursive call on a strict subterm. No termination proof; compiles to the recursor, emits equation lemmas. Mirror: the induction tactic.
Well-founded
Decreasing argument not a literal subterm (e.g. gcd on y % (x+1)). Supply termination_by + decreasing_by.
def mygcd : Nat → Nat → Nat
| 0, y => y
| x + 1, y => mygcd (y % (x + 1)) (x + 1)
termination_by a _ => a
decreasing_by exact Nat.mod_lt _ (Nat.succ_pos x)
partial def escapes the kernel and produces no equation lemmas — you can prove nothing about it.
macro "split_and" : tactic => `(tactic| refine ⟨?_, ?_⟩)
example (p q : Prop) (hp : p) (hq : q) : p ∧ q := by
split_and
· exact hp
· exact hq
Punchline: term mode and tactic mode, simp and grind, your macro and Mathlib's — all produce the same object, an Expr, and only the kernel confers theoremhood.
prove (P -> Q) -> P -> Q
Tactic it in the browser — intro f · intro p · apply f · exact p · qed — and qed extracts the λ-term with its principal type: the tactic script and the term are the same object. Then lean term_proofs shows the identical two-modes-one-Expr fact in Lean.
Recap & references
Generality via structures & typeclasses; search via exact?, Loogle, LeanSearch.
Automation: match each goal to the tactic that owns its shape — ring, nlinarith, omega, decide, grind.
calc for blackboard-legible proofs; structural vs. well-founded recursion; the de Bruijn kernel as sole arbiter.
Mathematics in Lean (Avigad et al.) · Theorem Proving in Lean 4 · Macbeth, The Mechanics of Proof & Algebraic Computations in Lean · Metaprogramming in Lean 4 · loogle.lean-lang.org · leansearch.net