Dependent Types and the Lambda Cube

So far our type systems have kept types and terms in separate worlds: terms compute, types classify, and the two never truly mingle. Dependent types tear down that wall. They let a type depend on a term — on an ordinary value. The classic example is the type of vectors of length n, written \mathsf{Vec}\,A\,n: here n is a runtime-ish number, yet it appears inside a type. Now append can have the type

\mathsf{append} : \Pi(n:\mathbb{N}).\ \Pi(m:\mathbb{N}).\ \mathsf{Vec}\,A\,n \to \mathsf{Vec}\,A\,m \to \mathsf{Vec}\,A\,(n+m),

a type that states and enforces "the output length is the sum of the input lengths". Off-by-one errors become type errors. Dependent types are the most expressive corner of the design space — powerful enough that types can encode arbitrary specifications, and the type checker becomes a proof checker. To see where they sit among everything we have built, we turn to Barendregt's lambda cube.

Π-types: the dependent function

The one binder that makes dependent types work is the dependent function type, or \Pi-type. Whereas the ordinary arrow A \to B describes a function whose result type B is fixed, \Pi(x:A).\,B(x) describes one whose result type B(x) may mention the argument value x:

\dfrac{\Gamma,\, x:A \vdash t : B(x)}{\Gamma \vdash \lambda x{:}A.\,t \;:\; \Pi(x{:}A).\,B(x)} \qquad \dfrac{\Gamma \vdash f : \Pi(x{:}A).\,B(x) \quad \Gamma \vdash a : A}{\Gamma \vdash f\,a : B(a)}

Two familiar things are special cases. When B does not depend on x, \Pi(x{:}A).\,B collapses to the plain arrow A \to B. And when A is itself the type of types, \Pi(X{:}\mathord{*}).\,B is exactly the polymorphic \forall X.\,B of System F. Universal quantification is just a dependent function that happens to take a type. Dually, the dependent pair \Sigma(x{:}A).\,B(x) — a value x together with evidence of type B(x) — generalises both the product and the existential \exists X.\,B.

The lambda cube: three axes of dependency

Start from the simply typed lambda calculus (\lambda_\to), where the only abstraction is "terms depending on terms" (ordinary functions). There are three independent ways to add more, and each is one axis of a cube. Switch on any subset of the three and you get one of 2^3 = 8 systems.

The far corner, with all three switched on, is \lambda P\omega = \lambda C, the Calculus of Constructions — the most expressive system of the cube and the theoretical core of the proof assistant Coq (and, extended, of Lean and Agda). System F_\omega (\lambda\omega) — polymorphism plus type operators, but no term-dependency — is the well-behaved system underneath Haskell's and Scala's type-level programming.

Curry–Howard: proofs are programs

Dependent types matter because of one of the deepest facts in logic and computing — the Curry–Howard correspondence: propositions are types, and proofs are programs. To prove a proposition is to construct a term of the corresponding type; the type checker, checking that term, is checking the proof.

LogicTypes
implication A \Rightarrow Bfunction A \to B
conjunction A \land Bproduct A \times B
disjunction A \lor Bsum / variant A + B
universal \forall x.\,P(x)dependent function \Pi(x{:}A).\,P(x)
existential \exists x.\,P(x)dependent pair \Sigma(x{:}A).\,P(x)
true / falseunit \mathbf{1} / empty \mathbf{0}

The \forall/\Pi and \exists/\Sigma rows are why we needed dependent types: only when a type can quantify over values can it express "for all numbers n…" — a real mathematical theorem. In a dependently typed language the code below is, quite literally, a handful of tiny proofs.

// Curry-Howard in miniature: propositions are types, proofs are terms. // A proof of A ⇒ A is the identity function (the simplest tautology). const identity = <A>(a: A): A => a; // Modus ponens: from a proof of A ⇒ B and a proof of A, derive B — just application. const modusPonens = <A, B>(imp: (a: A) => B, proofA: A): B => imp(proofA); // ∧-introduction is pairing; ∧-elimination is projection. const andIntro = <A, B>(a: A, b: B): [A, B] => [a, b]; const andElimL = <A, B>(p: [A, B]): A => p[0]; // ∨-introduction: injecting into a sum (a tagged union). type Or<A, B> = { tag: "left"; val: A } | { tag: "right"; val: B }; const orIntroR = <A, B>(b: B): Or<A, B> => ({ tag: "right", val: b }); console.log("proof of A⇒A on 7 :", identity(7)); console.log("modus ponens (·2) on 5 :", modusPonens((n: number) => n * 2, 5)); console.log("∧-elim-left of (3,'ok') :", andElimL(andIntro(3, "ok"))); console.log("∨-intro-right of true :", JSON.stringify(orIntroR<number, boolean>(true)));

Every line type-checks precisely because it is a valid proof. Add the ability to quantify over values — dependent types — and this same game scales up to proving the correctness of sorting algorithms, compilers (CompCert), and even the Four Colour Theorem. The type checker is the referee.

Yes — and it changed the field. CompCert, a C compiler by Xavier Leroy, is written and proved correct in Coq, whose kernel is the Calculus of Constructions sitting at the far corner of the lambda cube. The proof is a dependently typed term establishing that the compiled assembly behaves exactly like the source C program — so CompCert simply cannot introduce a miscompilation bug, a guarantee no amount of testing can give. When the famous "Csmith" fuzzing study threw millions of random C programs at production compilers, it found hundreds of bugs in GCC and LLVM — and none in CompCert's verified core. The Four Colour Theorem and the Feit–Thompson theorem have likewise been fully machine-checked in Coq. Curry–Howard is not a curiosity; it is how we now build software and mathematics we can actually trust.

Once a type can contain a term, deciding whether two types are equal may require running those terms. Is \mathsf{Vec}\,A\,(2+2) the same type as \mathsf{Vec}\,A\,4? Only if 2+2 reduces to 4 — so type equality is definitional (computational) equality, and the type checker must evaluate. This has a sharp consequence: if the term language allows non-termination, type checking can loop forever and become undecidable. That is why serious dependently typed languages (Coq, Agda, Idris, Lean) insist every function be total — provably terminating — enforced by a termination/positivity checker. It is the same totality we lost when recursive types made the calculus Turing-complete, now bought back deliberately: you cannot have unrestricted general recursion and sound, decidable dependent type checking at once. Dependent-type power is paid for in the discipline of totality.