VIASM Mini-Course · Hanoi 2026 · Lecture 5

Advanced Lean

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 * a once, 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$,

$$\prod_{x:\alpha} \beta\,x \quad\text{written}\quad (x : \alpha) \to \beta\,x \quad\text{or}\quad \forall\,x,\ \beta\,x .$$

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 n is a function sending each n to a proof of P n. Types depend on values — Vector α n carries its length in its type.

Prop vs Type — and why it matters

Everything lives in the universe hierarchy:

$$\mathrm{Prop} = \mathrm{Sort}\,0, \qquad \mathrm{Type}\,u = \mathrm{Sort}\,(u+1).$$

  • 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 [...]).
  • Looglesyntactic: search by shape, e.g. "(_ * _) ^ _". Complete and predictable.
  • LeanSearchneural/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.

One theorem end-to-end: $\sqrt 2$ is irrational

Standing on the library, it is one line:

theorem sqrt2_irrational : Irrational (Real.sqrt 2) := by
  exact_mod_cast Nat.Prime.irrational_sqrt (p := 2) Nat.prime_two

The instructive proof reconstructs the classical argument you now own:

$$2 q^2 = p^2 \Rightarrow 2 \mid p^2 \Rightarrow 2 \mid p \Rightarrow 4 \mid p^2 = 2q^2 \Rightarrow 2 \mid q,$$

contradicting $\gcd(p,q)=1$. The lesson: stand on Mathlib when the lemma exists, build it yourself when you need to see the machinery.

Under the hood: the elaboration pipeline

Lean 4 is written in Lean 4, and your code can run at every stage:

parse ▶ macro-expand ▶ elaborate ▶ kernel type-check

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