| ← Universes | Index | Next: Definitional vs propositional equality → |
Recall
Formal definitions cited in this section, gathered here for quick reference (full citations in the Bibliography):
- Progress (Theorem 9.3.5). “Suppose $t$ is a closed, well-typed term (that is, $\vdash t : T$ for some $T$). Then either $t$ is a value or else there is some $t’$ with $t \to t’$” (Pierce2002, §9.3 “Properties of Typing”).
- Preservation (Theorem 9.3.9). “If $\Gamma \vdash t : T$ and $t \to t’$, then $\Gamma \vdash t’ : T$” (Pierce2002, §9.3).
- Universe-formation rule. This book’s working statement, after the calculus of constructions (CoquandHuet1988): $\mathtt{Type}\,i : \mathtt{Type}\,(i+1)$, and a Π-type built from $A : \mathtt{Type}\,i$, $B : \mathtt{Type}\,j$ lands in $\mathtt{Type}\,(\max(i,j))$.
Chapters 1–4 have relied on Lean’s type checking constantly without ever seeing its rules written down. This section makes two things precise: the actual rules Lean’s kernel checks a term against (using a small, representative fragment, the simply typed λ-calculus, STLC), and the specific rule governing the universe hierarchy Section 2 just introduced informally.
An untyped calculus has a serious defect: it permits terms that reduce forever (Chapter 1, Section 4 mentioned β-reduction; nothing there stopped a term from β-reducing to itself, endlessly), and nothing prevents applying one kind of value where a completely different kind was intended. STLC fixes this by attaching a type to every term and requiring application to respect types — the direct ancestor of Chapter 1’s “every expression has a type.”
Types
\[\tau ::= \iota \;\mid\; \tau \to \tau\]where $\iota$ ranges over some fixed collection of base types (think
Nat, Bool) and $\tau_1 \to \tau_2$ is the type of functions from
$\tau_1$ to $\tau_2$ — exactly Lean’s →. Every type is built from base
types and arrows. There is not yet any way to quantify over all types
the way Chapter 1’s identity {α : Type} (x : α) : α := x did — that
extra generality is exactly Chapter 1, Section 5’s Π-types, already covered.
Programmer’s corner (Python), before the formal rules. Python’s own
type hints plus mypy are a light version of exactly this system, worth
seeing first since this Python tooling runs today, without any Lean
installation at all:
def apply_twice(f: int, x: int) -> int: # pretend f is Callable[[int], int]
...
def to_str(n: int) -> str:
return str(n)
# mypy accepts this: to_str's declared input type (int) matches what
# it is given (an int literal).
to_str(5)
# mypy rejects this with an error, WITHOUT running anything:
# error: Argument 1 to "to_str" has incompatible type "str"; expected "int"
to_str("already a string")
mypy’s check on to_str("already a string") is exactly the (App) rule
below, refusing to combine a function with an argument of the wrong type,
caught by reading the code rather than running it. The difference is that
Python’s hints are optional and only checked when mypy is run at all;
Lean’s type system is not optional, is checked on every single elaboration,
and is a real part of the language’s meaning rather than an add-on linter.
STLC below is what is actually going on, underneath both.
Typing judgments and rules
A typing judgment $\Gamma \vdash t : \tau$ reads “in context $\Gamma$
(a list of variable-type assignments $x_1:\tau_1, \ldots, x_n:\tau_n$), the
term $t$ has type $\tau$.” This is precisely what Lean’s #check reports,
and precisely the “context” that appears above the line in every tactic
goal state since Chapter 4. The rules that generate valid judgments:
Each rule, read as a Lean fact already familiar from earlier chapters:
- (Var) — if
x’s type is recorded in the local context,#check xreports that type; there is nothing to derive. - (Abs) — to type-check
fun x => t, extend the context withx : τ1(a fresh assumption, exactly like a hypothesis introduced byintroin Chapter 4), check thatt’s type isτ2under that extended context, and conclude the whole abstraction has typeτ1 → τ2. This is how Lean checks everydef f (x : τ1) : τ2 := twritten since Chapter 1. - (App) — to type-check
t1 t2,t1must have a function type whose domain matchest2’s type exactly; the result has the codomain type. This is exactly the error Chapter 4 discussed under “reading a tactic failure”:exactefails with a type mismatch precisely when this rule’s side condition (that $\tau_1$ must match on both sides) is not met.
Type preservation and progress: the payoff
Two theorems about STLC are the entire reason to bother with a type system at all:
- Progress: a well-typed closed term (no free variables) is either
already a value — an abstraction, or (if base types come with their
own constants, as
Nat/Booleffectively do) a constant of a base type — or it can take a β-reduction step. It never “gets stuck” partway through evaluation. There is no well-typed analogue of “apply3totrue,” because (App)’s side condition would already have rejected such a term at elaboration time, before any reduction is attempted. - Preservation (subject reduction): if $\Gamma \vdash t : \tau$ and
$t \longrightarrow_\beta t’$, then $\Gamma \vdash t’ : \tau$. Reduction
never changes a term’s type. This is exactly why this chapter’s own
definitional equality (Section 4, next) is trustworthy: reducing a term to
normal form (what
#eval/rfldo) can never accidentally produce something of a different type than the one it started with.
Together, progress and preservation are the formal content of “well-typed programs do not go wrong,” and, viewed through Curry–Howard (Chapter 3), “well-typed proof terms do not prove something other than what they claim to prove.” The entire reliability of Lean as a proof assistant rests on an extended version of exactly these two theorems, proved once for Lean’s kernel and then trusted for every proof checked thereafter.
What STLC still cannot do
STLC cannot type identity from Chapter 1 polymorphically. One could
write identity_Nat : Nat → Nat and separately identity_Bool : Bool → Bool,
one arrow-type definition per base type. But there is no single term of a
single STLC type that captures “the identity function, at every type.”
Programmer’s corner (Python), on the same limitation. Plain Python
never runs into this, because it has no static types to begin with — def
identity(x): return x already works on anything, at runtime, with zero
declarations. But the instant type hints are added, wanting mypy to
check the general claim “this returns whatever type it was given”
requires a dedicated feature, TypeVar, precisely because bare hints have
the same limitation as STLC:
from typing import TypeVar
T = TypeVar("T")
def identity(x: T) -> T: # one signature, valid at every type
return x
identity(5) # T := int
identity("hello") # T := str
Without TypeVar, the only option would be writing identity_int(x: int)
-> int and identity_str(x: str) -> str separately — exactly STLC’s
one-arrow-type-per-base-type wall. TypeVar is Python typing’s escape
hatch for this specific gap, and it is a useful anchor: a much lighter
version of the same extra generality Lean’s identity {α : Type} (x : α) :
α := x uses, where α is filled in silently every call, the same way
mypy silently solves T := int above.
This is precisely the gap Chapter 1, Section 5
already closed: dependent types let a type itself depend on a term
(here, the type argument α). That is exactly the extra generality
identity {α : Type} (x : α) : α := x uses, and it is unavailable in STLC
(or in Python’s TypeVar, which is real but considerably less powerful —
it cannot let a return type depend on an ordinary value argument the
way Chapter 1, Section 3’s Vec.replicate does).
Universes, as a typing rule
Section 2 introduced the hierarchy $\mathtt{Type} : \mathtt{Type}\,1 :
\mathtt{Type}\,2 : \cdots$ to avoid a Russell-style paradox (a “type of all
types” that contains itself leads to the same contradiction as “the set of
all sets that do not contain themselves”), and showed Group : Type →
Type had to live in Type 1. In the calculus of constructions — the
formal system CoC/CIC that Chapter 1, Section 5 named, extending STLC above with
Π-types and universes — this is stated as a typing rule for the universes
themselves:
together with a rule saying Π-types (function types, including ordinary
→) stay inside a suitable universe:
This is exactly Section 2’s Group : Type → Type computation spelled out as a
general rule: with $A = \mathtt{Type}$ (itself living in Type 1) and
$B = \mathtt{Type}$ again, the rule gives $\max(1, 1) = 1$, so Type →
Type lands in Type 1, one level above Type itself.
Programmer’s corner (Python), on why this is genuinely a type-theory
concern and not a Python one. In Python, type(3) is int, and
type(int) is type — and, unlike Lean’s stratified hierarchy,
type(type) is also type:
>>> type(3)
<class 'int'>
>>> type(int)
<class 'type'>
>>> type(type)
<class 'type'>
Python allows type to be its own type, with no stratification at all,
because Python’s type system is not being used as a proof system — there
is no soundness property at stake that a Russell-style paradox could
break. Lean’s Type cannot self-apply this way (Type : Type is
inconsistent — it allows encoding Girard’s paradox and proving False),
which is exactly why the infinite, strictly increasing hierarchy above is
load-bearing rather than pedantry. This is one of the few places where the
Python comparison genuinely runs out: it is not that Python does the same
thing more simply, it is that Python does not need to solve this problem
at all, because nothing checks proofs against it.
References
Full citations in the Bibliography. Formal definitions are gathered in Recall, above.
- Pierce (Pierce2002), Ch. 8 “Typed Arithmetic Expressions” §8.3 “Safety = Progress + Preservation” (Theorems 8.3.2/8.3.3, first proved there for a smaller language) and Ch. 9 “Simply Typed Lambda-Calculus” §9.2 “The Typing Relation” (the (T-Var)/(T-Abs)/(T-App) rules) and §9.3 “Properties of Typing” (Theorems 9.3.5/9.3.9, progress/preservation restated for STLC) — verified verbatim. An earlier draft of this section cited Ch. 9–11; Ch. 11 “Simple Extensions” actually covers pairs/tuples/records/sums, unrelated to this section’s content.
- Milner (Milner1978) — the theoretical background for why STLC alone cannot type polymorphic functions like
identity. - Python
typingmodule documentation and mypy documentation (PythonTyping, MypyDocs) — for the Python-side comparison used in this section’s boxes. - Coquand and Huet (CoquandHuet1988) — the original paper defining CoC, whose universe-formation rule is stated above.
- Girard — see Chapter 5, Section 2’s References for the full citation: “Girard’s paradox” (the inconsistency of
Type : Type) is due to Girard’s 1972 thesis, a different, later paper than Girard1971 cited elsewhere in this book. - Theorem Proving in Lean 4 (TPIL4), §2.2 “Types as objects” — Lean’s own documentation on universes, matching the presentation here.
| ← Universes | Index | Next: Definitional vs propositional equality → |