MathLabs

Foundations of mathematics

Type theory

A foundation for mathematics and computation where every term carries a type, used in proof assistants.

IntuitionEvery value has a label

In most programming languages, 33 is an `int` and `"hello"` is a `string` — the compiler rejects `3 + "hello"` before the program ever runs. Type theory turns this everyday idea into a foundation for all of mathematics: instead of starting from sets and membership (x∈Ax \in A), start from terms and types (t:At : A, "tt has type AA"). Remarkably, once types are rich enough, a type can encode a mathematical proposition, and a term of that type becomes a proof — programs and proofs become the same kind of object.

Tree diagram of a typing derivation with typing judgments at each node.
A typing derivation as a tree: each node is a judgment Γ⊢t:A\Gamma \vdash t : A, each edge a typing rule connecting a term to its subterms.

UndergraduateSimply typed lambda calculus

Definition: Typing judgment

A typing judgment Γ⊢t:A\Gamma \vdash t : A says "in context Γ\Gamma (a list of variable-type pairs x1:A1,…,xn:Anx_1:A_1,\dots,x_n:A_n), term tt has type AA". The core rules: a variable has whatever type its context assigns it; an abstraction λx:A. t\lambda x{:}A.\,t has type A→BA\to B whenever tt has type BB in the context extended with x:Ax:A; and application f uf\,u has type BB whenever f:A→Bf:A\to B and u:Au:A.

Γ,x:A⊢t:BΓ⊢λx:A. t:A→BΓ⊢f:A→BΓ⊢u:AΓ⊢f u:B\dfrac{\Gamma, x{:}A \vdash t : B}{\Gamma \vdash \lambda x{:}A.\,t : A\to B} \qquad \dfrac{\Gamma \vdash f : A\to B \quad \Gamma \vdash u : A}{\Gamma \vdash f\,u : B}

Martin-Löf dependent type theory generalizes this further: the result type of a function can depend on the value of its argument. The **Π\Pi-type** (dependent function type) ∏x:AB(x)\prod_{x:A} B(x) collects functions sending each x:Ax:A to a term of type B(x)B(x) — when BB doesn't mention xx, this collapses to the ordinary A→BA\to B. Dually, the **Σ\Sigma-type** (dependent pair type) ∑x:AB(x)\sum_{x:A} B(x) collects pairs ⟨a,b⟩\langle a,b\rangle with a:Aa:A and b:B(a)b:B(a) — a pair whose second component's type depends on the first; when BB doesn't mention xx, this collapses to the ordinary product A×BA\times B.

∏x:AB(x),∑x:AB(x)\prod_{x:A} B(x), \qquad \sum_{x:A} B(x)
Dependent types and their Curry–Howard reading
Type constructorNon-dependent special caseCurry–Howard reading
∏x:AB(x)\prod_{x:A} B(x)A→BA\to B (when BB is constant)∀x:A, B(x)\forall x{:}A,\ B(x)
∑x:AB(x)\sum_{x:A} B(x)A×BA\times B (when BB is constant)∃x:A, B(x)\exists x{:}A,\ B(x)
Univalence: (A≃B)≃(A=UB)(A\simeq B) \simeq (A =_{\mathcal U} B)—Equivalent types are identified

If Γ⊢t:A\Gamma \vdash t : A in the simply typed lambda calculus, then tt is strongly normalizing: every sequence of β\beta-reductions starting from tt terminates after finitely many steps.

Why is it true?

This is why simply typed lambda calculus, despite having function application and recursion-like nesting, is not Turing-complete — no well-typed term can loop forever, which is exactly what makes type-checkers in this fragment always terminate.

Proof

(Tait's reducibility method.) Define, by induction on the structure of types, a set REDA\mathrm{RED}_A of "reducible terms" for every type AA: for a base type oo, let REDo\mathrm{RED}_o be the set SNSN of all strongly normalizing terms; for a function type, let REDA→B={t:for every u∈REDA, t u∈REDB}\mathrm{RED}_{A\to B} = \{t : \text{for every } u \in \mathrm{RED}_A,\ t\,u \in \mathrm{RED}_B\}.

One first proves, by induction on AA, three technical properties together: (CR1) every term in REDA\mathrm{RED}_A is strongly normalizing; (CR2) REDA\mathrm{RED}_A is closed under β\beta-reduction (if t∈REDAt\in\mathrm{RED}_A and t→t′t\to t' then t′∈REDAt'\in\mathrm{RED}_A); (CR3) any "neutral" term (a variable or application, not an abstraction) all of whose one-step reducts already lie in REDA\mathrm{RED}_A must itself lie in REDA\mathrm{RED}_A. The base case is immediate from the definition of SNSN; the function-type case unfolds the definition of REDA→B\mathrm{RED}_{A\to B} and uses the induction hypothesis on the smaller types AA and BB.

Next, one shows every well-typed term is reducible under any substitution of reducible terms for its free variables: by induction on the typing derivation of Γ⊢t:A\Gamma \vdash t:A, if σ\sigma assigns each variable xx in Γ\Gamma a term σ(x)∈REDΓ(x)\sigma(x)\in\mathrm{RED}_{\Gamma(x)}, then tσ∈REDAt\sigma \in \mathrm{RED}_A. The variable case is immediate. The application case follows directly from the definition of REDA→B\mathrm{RED}_{A\to B}. The abstraction case needs the most care: for (λx. t)σ(\lambda x.\,t)\sigma applied to any u∈REDAu\in\mathrm{RED}_A, the term reduces in one β\beta-step to t(σ,x:=u)t(\sigma,x{:=}u), which is in REDB\mathrm{RED}_B by the induction hypothesis (applied to the extended substitution); property (CR3) — closure under "expansion" for terms whose reducts are all already reducible — then places (λx. t)σ u(\lambda x.\,t)\sigma\,u itself in REDB\mathrm{RED}_B, so (λx. t)σ∈REDA→B(\lambda x.\,t)\sigma \in \mathrm{RED}_{A\to B} by definition.

Finally, apply this to the identity substitution: every variable x:Ax{:}A is itself neutral with no reducts at all, so it is vacuously in REDA\mathrm{RED}_A by (CR3). So for any closed derivation Γ⊢t:A\Gamma \vdash t:A, taking σ\sigma to map each xx to itself gives t=tσ∈REDAt = t\sigma \in \mathrm{RED}_A. By (CR1), REDA⊆SN\mathrm{RED}_A \subseteq SN, so t∈SNt \in SN: every well-typed term is strongly normalizing. ■\blacksquare

AdvancedThe Curry–Howard correspondence

Read A→BA\to B as "if AA then BB", read A×BA\times B as "AA and BB", and a proof of a proposition becomes a well-typed program of the matching type — this is propositions-as-types, proofs-as-programs. Checking a proof reduces to type-checking a term; constructing a proof reduces to writing a program of the right type.

Let φ\varphi be a propositional formula built from atoms, ∧\wedge, and →\to, and let AφA_\varphi be the simple type obtained by translating atoms to base types, ∧\wedge to product ×\times, and →\to to function type →\to. Then φ\varphi is provable in intuitionistic propositional logic if and only if AφA_\varphi is inhabited: there is a closed term tt with ⊢t:Aφ\vdash t : A_\varphi.

Why is it true?

This is the precise statement behind "a proof assistant is just a type-checker": Coq, Agda, and Lean accept a machine-checked proof of a theorem exactly when they accept a term of the corresponding type, and type-checking (unlike theorem-proving in general) is a small, fast, mechanical, and highly trustworthy algorithm.

Proof

(Proofs become programs.) By induction on a natural-deduction derivation of ⊢φ\vdash \varphi. If the last rule is →\to-introduction, deriving φ=ψ→χ\varphi=\psi\to\chi from a subproof of χ\chi under hypothesis ψ\psi: by the induction hypothesis that subproof translates to a term tt with x:Aψ⊢t:Aχx{:}A_\psi \vdash t : A_\chi (the hypothesis becomes a free variable xx); then λx:Aψ. t\lambda x{:}A_\psi.\,t is closed and has type Aψ→Aχ=AφA_\psi\to A_\chi=A_\varphi. If the last rule is →\to-elimination (modus ponens) from ψ→χ\psi\to\chi and ψ\psi: by the induction hypothesis we have terms t1:Aψ→Aχt_1 : A_\psi\to A_\chi and t2:Aψt_2 : A_\psi; then t1 t2:Aχt_1\,t_2 : A_\chi. The ∧\wedge-introduction and ∧\wedge-elimination rules translate symmetrically to pairing ⟨t1,t2⟩\langle t_1,t_2\rangle and the two projections. Every rule of the proof system has a matching term-building rule, so induction on the derivation produces a well-typed closed term of type AφA_\varphi.

(Programs become proofs.) Conversely, by induction on a typing derivation ⊢t:A\vdash t : A. Each typing rule — variable, abstraction, application, pairing, projection — has exactly the same tree shape as a natural-deduction rule — hypothesis, →\to-introduction, →\to-elimination, ∧\wedge-introduction, ∧\wedge-elimination. So take the same derivation tree, erase all the terms, and read each type as the formula it translates from: this is precisely a valid intuitionistic proof of φ\varphi (where A=AφA=A_\varphi).

Since the two translations (proof →\to term, and term →\to proof) are structural inverses of each other on derivation trees — each one undoes exactly what the other does, rule by rule — φ\varphi is provable exactly when AφA_\varphi is inhabited. ■\blacksquare

UndergraduateReal-World Applications and Worked Examples

Rich type systems catch entire classes of bugs before a program ever runs: Rust's ownership types prevent use-after-free and data races at compile time, and dependently typed languages can encode invariants like "this list has exactly nn elements" or "this index is within bounds" directly into types, making certain runtime errors unrepresentable. The deepest application is inside proof assistants themselves: Coq, Agda, and Lean have small, carefully audited kernels that do nothing but type-check terms — because of the Curry–Howard correspondence, accepting a term of a theorem's type is accepting a machine-checked proof, so trusting a mathematical result the size of the Feit–Thompson theorem or the four-color theorem reduces to trusting a few hundred lines of type-checking code, not the (much larger, much harder to audit) tactics that built the proof term.

Example: A Π\Pi-type that ordinary function types cannot express

Let Vec A n\mathrm{Vec}\,A\,n be the type of length-nn lists of elements of type AA. Write down a term inhabiting ∏n:N(Vec A n→Vec A n)\prod_{n:\mathbb N} (\mathrm{Vec}\,A\,n \to \mathrm{Vec}\,A\,n) — "for every length nn, a function on length-nn vectors" — and explain why simple types (without Π\Pi) cannot express this specification at all.

Solution

The term λn:N. λv:Vec A n. v\lambda n{:}\mathbb N.\,\lambda v{:}\mathrm{Vec}\,A\,n.\,v works: for each natural number nn, it takes a length-nn vector vv and returns it unchanged — plainly well-typed, since v:Vec A nv : \mathrm{Vec}\,A\,n is returned at type Vec A n\mathrm{Vec}\,A\,n, for whichever nn was supplied.

What makes this a genuine use of Π\Pi rather than an ordinary function type is that the type of the second argument (Vec A n\mathrm{Vec}\,A\,n) mentions the value of the first argument (nn). In simply typed lambda calculus, a function's argument and result types are fixed once and for all — A→BA\to B with A,BA,B chosen before any value of type AA is known. There is no way to write "give me a natural number nn, and depending on which number you gave me, I will demand a vector of that exact length" using only →\to: you would need a single fixed type Vec A n\mathrm{Vec}\,A\,n that somehow works for every nn simultaneously, which is not what Vec\mathrm{Vec} means.

Concretely, if you tried to write this using ordinary function types, you could at best get N→(Vec A ?→Vec A ?)\mathbb N \to (\mathrm{Vec}\,A\,? \to \mathrm{Vec}\,A\,?) for one FIXED placeholder length — useless, since it would reject vectors of any other length, or accept vectors of the wrong length. The Π\Pi-type ∏n:N(⋯ )\prod_{n:\mathbb N}(\cdots) is precisely the type constructor that lets the codomain "look back" at the specific value of nn received, which is exactly the dependency simple types cannot express.

Example: Curry–Howard for function composition

The formula (A→B)→((B→C)→(A→C))(A\to B)\to((B\to C)\to(A\to C)) expresses transitivity of implication. Under Curry–Howard, find the λ\lambda-term that proves it, and verify its type derivation step by step.

Solution

The term is t=λf:A→B. λg:B→C. λx:A. g (f x)t = \lambda f{:}A\to B.\,\lambda g{:}B\to C.\,\lambda x{:}A.\,g\,(f\,x) — exactly function composition g∘fg\circ f applied to xx.

Derive the type from the inside out. In context f:A→B, g:B→C, x:Af{:}A\to B,\,g{:}B\to C,\,x{:}A: since f:A→Bf:A\to B and x:Ax:A, application gives f x:Bf\,x:B. Since g:B→Cg:B\to C and f x:Bf\,x:B, application gives g (f x):Cg\,(f\,x):C.

Now discharge the abstractions from the inside out. λx:A. g (f x)\lambda x{:}A.\,g\,(f\,x) has type A→CA\to C in context f:A→B, g:B→Cf{:}A\to B,\,g{:}B\to C. Then λg:B→C. (⋯ )\lambda g{:}B\to C.\,(\cdots) has type (B→C)→(A→C)(B\to C)\to(A\to C) in context f:A→Bf{:}A\to B. Finally λf:A→B. (⋯ )\lambda f{:}A\to B.\,(\cdots) has type (A→B)→((B→C)→(A→C))(A\to B)\to((B\to C)\to(A\to C)) with no remaining free assumptions — a closed term.

So ⊢t:(A→B)→((B→C)→(A→C))\vdash t : (A\to B)\to((B\to C)\to(A\to C)), meaning tt is a Curry–Howard proof of transitivity: "if AA implies BB, and BB implies CC, then AA implies CC" — the same term that a functional programmer would write to compose two functions.

ResearchResearch today

In simply typed lambda calculus, if f:A→Bf : A \to B and a:Aa : A, what is the type of f af\,a?

Why can a proof assistant like Coq, Agda, or Lean trust a machine-checked proof thousands of lines long?

When B(x)B(x) does not actually depend on xx, what does ∏x:AB(x)\prod_{x:A} B(x) reduce to?

The Univalence Axiom (A≃B)≃(A=UB)(A\simeq B) \simeq (A =_{\mathcal U} B) says that...

References

  1. The Univalent Foundations Program (2013). Homotopy Type Theory: Univalent Foundations of Mathematics
  2. Wikipedia contributors (2024). Curry–Howard correspondence
  3. Wikipedia contributors (2024). Intuitionistic type theory