Summary
This article explores the Curry-Howard correspondence, a profound connection between type theory and mathematical logic where types are propositions, programs are proofs, and evaluation is proof normalization.
Key Concepts:
-
Fundamental Mappings:
- Implication (P ⇒ Q) ↔ Function types (A → B)
- Conjunction (P ∧ Q) ↔ Product types (A × B)
- Disjunction (P ∨ Q) ↔ Sum types (A + B)
- Universal quantification (∀x.P) ↔ Dependent products (Πx:A.B)
- Existential quantification (∃x.P) ↔ Dependent sums (Σx:A.B)
-
Lambda Calculus & Logic:
- Simply typed lambda calculus corresponds to intuitionistic propositional logic via natural deduction
- Each typing rule is an inference rule; each program is a constructive proof
- Classic combinators (K, S) prove logical theorems
-
Intuitionistic vs Classical Logic:
- Curry-Howard works for intuitionistic logic (constructive proofs)
- Law of excluded middle and double negation elimination not universally provable
- Ensures all proofs are constructive and computable
-
Dependent Types:
- Enable expressing first-order logic in types
- Vector types prove properties like length preservation in append
- Used in proof assistants: Coq, Agda, Lean, Idris
-
Linear Logic & Linear Types:
- Resources used exactly once
- Rust's ownership system inspired by linear types
- Session types ensure protocol correctness at compile time
-
Practical Applications:
- CompCert: Formally verified C compiler with correctness as a proven theorem
- seL4: Verified microkernel with proven security properties
- Smart Contracts: Type-level verification of financial invariants
- Type-Safe Queries: Database libraries that prove query correctness
-
Philosophical Implications:
- Programs are mathematical objects
- Correctness is compositional through types
- The uncomputable corresponds to the unprovable (Gödel's theorems)
- Type signatures are theorems; implementations are proofs
Profound Insight: The Curry-Howard correspondence reveals that programming and mathematics are not separate disciplines but unified—every well-typed program is a theorem, every proof is an algorithm. This perspective enables verified software where "it compiles" genuinely means "it's correct."
Impact: From verified compilers and operating systems to formally proven cryptographic implementations, Curry-Howard is transforming how we build reliable software, making the dream of mathematically proven correctness a practical reality.
Type Systems as Proofs: The Curry-Howard Correspondence
Introduction: A Bridge Between Worlds
In 1934, Haskell Curry observed a striking similarity between the types of combinators and axioms in propositional logic. Nearly three decades later, William Howard extended this observation into what we now call the Curry-Howard correspondence (also known as the Curry-Howard isomorphism or propositions-as-types interpretation).
This correspondence reveals a deep, almost mystical connection:
- Types are propositions
- Programs are proofs
- Evaluation is proof normalization
This isn't mere analogy—it's a precise mathematical relationship that has transformed our understanding of both programming languages and logic. When you write a well-typed program, you're constructing a mathematical proof. When you prove a theorem, you're writing a program.
The Fundamental Correspondence
Let's start with the basic mappings between logic and type theory:
| Logic | Type Theory | Programming |
|---|---|---|
| Proposition P | Type A | Type annotation |
| Proof of P | Term of type A | Value/Program |
| P ⇒ Q (implication) | A → B (function type) | Function |
| P ∧ Q (conjunction) | A × B (product type) | Pair/Tuple |
| P ∨ Q (disjunction) | A + B (sum type) | Union/Variant |
| ⊤ (true) | Unit type | unit/void |
| ⊥ (false) | Empty type | Never |
| ∀x.P(x) (universal) | Πx:A.B(x) (dependent product) | Generic function |
| ∃x.P(x) (existential) | Σx:A.B(x) (dependent sum) | Existential type |
Implication as Function Types
The most fundamental correspondence is between logical implication and function types. Consider the proposition "If it rains, then the ground is wet" (P ⇒ Q). In type theory, this becomes a function type Rain → Wet.
// The proposition "P implies Q" as a function type
;
// A proof that "if we have an A, we can get a B"
// Example: Proof of transitivity (A → B) → (B → C) → (A → C)
Conjunction as Product Types
The logical conjunction "P and Q" (P ∧ Q) corresponds to the product type (pair). To prove P ∧ Q, you need both a proof of P and a proof of Q.
// Conjunction as a pair type
;
// Proof that P ∧ Q implies P (first projection)
// Proof that P ∧ Q implies Q (second projection)
// Proof that P → Q → (P ∧ Q) (pair introduction)
// Proof that (P ∧ Q) → (Q ∧ P) (commutativity)
Disjunction as Sum Types
The logical disjunction "P or Q" (P ∨ Q) corresponds to sum types (tagged unions). To prove P ∨ Q, you need either a proof of P or a proof of Q.
// Disjunction as a tagged union
;
// Proof that P implies (P ∨ Q)
// Proof that Q implies (P ∨ Q)
// Proof by cases: (P ∨ Q) → (P → R) → (Q → R) → R
Simply Typed Lambda Calculus and Natural Deduction
The simply typed lambda calculus (STLC) precisely corresponds to intuitionistic propositional logic via natural deduction. Each typing rule in STLC corresponds to an inference rule in natural deduction.
Natural Deduction Rules as Typing Rules
// AXIOM RULE (Variable)
// In natural deduction: If P is assumed, then P is proven
// In type theory: If x has type A in context, then x has type A
// IMPLICATION INTRODUCTION (→I, Abstraction)
// To prove P → Q, assume P and prove Q
// To construct a function A → B, take parameter of type A and return B
// IMPLICATION ELIMINATION (→E, Application, Modus Ponens)
// If we have P → Q and P, we can derive Q
// If we have f: A → B and a: A, we can get f(a): B
Example: Proving the K Combinator
The K combinator (constant function) proves the theorem P → (Q → P):
// K combinator: λx.λy.x
// Type: A → B → A
// Logical reading: P → (Q → P)
// Meaning: "If P is true, then P is true regardless of Q"
// This is a proof that if we have a proof of P,
// we can always produce a proof of P even when given
// an additional hypothesis Q
Example: Proving the S Combinator
The S combinator proves a more complex theorem:
// S combinator: λx.λy.λz.x z (y z)
// Type: (A → B → C) → (A → B) → A → C
// Logical reading: (P → Q → R) → (P → Q) → P → R
// This proves: "If P implies (Q implies R), and P implies Q,
// then P implies R"
// This is a form of hypothetical syllogism
Intuitionistic vs Classical Logic
The Curry-Howard correspondence works for intuitionistic logic, not classical logic. The key difference is the law of excluded middle (LEM): P ∨ ¬P.
In classical logic, LEM is an axiom. In intuitionistic logic, LEM is not universally true—you can only assert P ∨ ¬P if you have a specific proof of either P or ¬P.
// In intuitionistic type theory, we CANNOT write:
// function lem<P>(): Or<P, Not<P>> { ... }
// because we don't have a general way to decide arbitrary propositions
// However, for specific decidable types, we can:
;
// Double negation elimination also fails in general:
// We CAN prove: P → ¬¬P
// But we CANNOT prove: ¬¬P → P (in general)
// function doubleNegElim<P>(nnp: Not<Not<P>>): P {
// // No way to construct a value of type P from nnp!
// }
This restriction to intuitionistic logic is not a bug—it's a feature! It ensures that every proof is constructive: a proof of ∃x.P(x) must actually construct an x, not just prove that such an x must exist.
Dependent Types: The Full Power of Curry-Howard
The correspondence becomes even more powerful with dependent types, where types can depend on values. This corresponds to first-order predicate logic.
Dependent Function Types (Π-types)
A dependent function type Πx:A.B(x) represents a function that, given x of type A, returns a value of type B(x) where B depends on x. This corresponds to universal quantification ∀x:A.P(x).
// Pseudo-TypeScript with dependent types (like in Agda/Coq/Lean)
// This isn't valid TypeScript, but illustrates the concept
// Universal quantification: ∀n:Nat. P(n)
// As a dependent function: (n: Nat) => P(n)
// Proof that ∀n. Vec<T, n> can be reversed to Vec<T, n>
Dependent Pair Types (Σ-types)
A dependent pair type Σx:A.B(x) represents a pair (x, y) where x has type A and y has type B(x). This corresponds to existential quantification ∃x:A.P(x).
// Existential quantification: ∃n:Nat. Vec<T, n>
// "There exists a natural number n such that we have a vector of length n"
// Constructing an existential proof
Example in Agda-like Syntax
-- Agda-like pseudocode
-- Proving that vector append has the right length
data Vec (A : Set) : Nat → Set where
[] : Vec A zero
_++_ : {A : Set} {m n : Nat} → Vec A m → Vec A n → Vec A (m + n)
[] ++ ys = ys
(x :: xs) ++ ys = x :: (xs ++ ys)
-- The type signature is a theorem:
-- "For all types A and numbers m, n,
-- if you have a vector of length m and a vector of length n,
-- then you can produce a vector of length m+n"
-- The function definition is the PROOF of this theorem!
Proof Assistants: Curry-Howard in Practice
Modern proof assistants like Coq, Agda, Lean, and Idris are programming languages based on the Curry-Howard correspondence. When you write code in these languages, you're simultaneously writing proofs.
Example: Proving List Properties in Coq
(* Coq proof that list reversal is involutive *)
Require Import List.
Import ListNotations.
(* Helper lemma: reverse of append *)
Lemma rev_append_distr : forall A (l1 l2 : list A),
rev (l1 ++ l2) = rev l2 ++ rev l1.
Proof.
intros A l1 l2.
induction l1 as [| h1 t1 IH].
- (* l1 = [] *)
simpl.
rewrite app_nil_r.
reflexivity.
- (* l1 = h1 :: t1 *)
simpl.
rewrite IH.
rewrite app_assoc.
reflexivity.
Qed.
(* Main theorem: reverse is involutive *)
Theorem rev_involutive : forall A (l : list A),
rev (rev l) = l.
Proof.
intros A l.
induction l as [| h t IH].
- (* l = [] *)
simpl.
reflexivity.
- (* l = h :: t *)
simpl.
rewrite rev_append_distr.
rewrite IH.
simpl.
reflexivity.
Qed.
Each Proof block is a program that constructs evidence for the theorem. The tactics (intros, induction, rewrite, reflexivity) are instructions for building this proof term.
Example: Arithmetic in Lean 4
-- Lean 4 proof that addition is commutative
(m n : Nat) : m + n = n + m := by
induction n with
| zero =>
-- Base case: m + 0 = 0 + m
rw [Nat.add_zero, Nat.zero_add]
| succ n ih =>
-- Inductive case: m + (n+1) = (n+1) + m
rw [Nat.add_succ, ih, Nat.succ_add]
-- Proof that multiplication distributes over addition
(m n k : Nat) : m * (n + k) = m * n + m * k := by
induction k with
| zero =>
rw [Nat.add_zero, Nat.mul_zero, Nat.add_zero]
| succ k ih =>
rw [Nat.add_succ, Nat.mul_succ, ih, Nat.mul_succ, Nat.add_assoc]
-- Using dependent types to prove vector properties
(α : Type) (n : Nat) := { l : List α // l.length = n }
{α : Type} {m n : Nat} (v1 : Vec α m) (v2 : Vec α n) : Vec α (m + n) :=
⟨v1.val ++ v2.val, by
simp [List.length_append, v1.property, v2.property]⟩
Linear Types and Linear Logic
Linear logic, introduced by Jean-Yves Girard, corresponds to linear type systems where resources must be used exactly once. This has profound implications for resource management.
// Rust's ownership system is inspired by linear types
// Values are used exactly once (or explicitly copied/cloned)
// Linear implication: A ⊸ B
// "A implies B, using A exactly once"
// In Rust: fn(A) -> B where A is moved
// Multiplicative conjunction: A ⊗ B
// "Both A and B, each usable once"
// In Rust: (A, B) where both are moved when used
// Additive conjunction: A & B
// "Choose to use either A or B"
// In Rust: enum Either<A, B> { Left(A), Right(B) }
Session Types
Linear types enable session types, which ensure protocol correctness at compile time:
// Simplified session types in Rust
// State machine encoded in types
;
;
;
// Usage enforces correct protocol:
Practical Applications
1. Verified Compilers
CompCert is a formally verified C compiler written in Coq. Its correctness is a proven theorem:
Theorem transf_c_program_correct:
forall p tp,
transf_c_program p = OK tp ->
forward_simulation (Csem.semantics p) (Asm.semantics tp).
This theorem states: "If the compiler successfully compiles program p to tp, then the assembly semantics of tp is a forward simulation of the C semantics of p." The proof of this theorem IS the compiler.
2. Verified Operating Systems
seL4 is a formally verified microkernel. Properties like "isolation between processes" are proven theorems:
theorem integrity:
"⟦ valid_state s; (s, s') ∈ data_type.step; pas_wellformed pas ⟧
⟹ (∀d∈subjects_of_state s. integrity_check s s' d pas)"
3. Smart Contract Verification
Using Curry-Howard to verify smart contracts:
// Property we want to prove:
// "After a valid transfer, sender balance decreases and receiver balance increases"
// The theorem (as a type)
;
// The function implementation IS the proof
4. Type-Safe Database Queries
Libraries like Prisma use the Curry-Howard correspondence to ensure query safety:
// The type system proves that this query is well-formed
;
// Result type is automatically inferred as:
// Array<{ id: number; name: string }>
// The type system has PROVEN that only id and name are selected
The Philosophy: Programs as Mathematical Objects
The Curry-Howard correspondence reveals programming as a branch of mathematics. Every well-typed program is a theorem; every proof is an algorithm.
This perspective offers several profound insights:
1. Correctness is Compositional
Just as you can build complex proofs from simpler lemmas, you can build correct programs from correct components. Type safety guarantees this composition preserves correctness.
// If f: A → B is correct (proves P → Q)
// And g: B → C is correct (proves Q → R)
// Then g ∘ f: A → C is automatically correct (proves P → R)
2. The Uncomputable is Unprovable
By Gödel's incompleteness theorems, there exist true statements that cannot be proven. By Curry-Howard, there exist types that have no inhabitants (no programs of that type).
// The halting problem: no function can decide if arbitrary programs halt
// This corresponds to the logical statement being unprovable
;
;
// This type has NO inhabitants (cannot be implemented):
;
// We cannot write a total function with this type
3. Types as Contracts
A type signature is a contract—a precise specification of what a function promises. The implementation is a proof that the contract can be fulfilled.
-- Haskell: the type signature is a theorem
-- The implementation is the proof
-- Theorem: "Lists form a monoid under concatenation"
mempty = [] -- Proof of identity element
mappend = (++) -- Proof of associative operation
-- The type checker verifies this proof is correct
Conclusion: The Unity of Computation and Logic
The Curry-Howard correspondence isn't just a curiosity—it's a fundamental insight into the nature of computation and reasoning. It tells us that:
- Every program is a proof: When you write well-typed code, you're constructing mathematical arguments
- Every proof is a program: Mathematical reasoning is computational
- Types are propositions: Type checking is theorem proving
- Execution is normalization: Running a program is simplifying a proof
This unity has practical implications:
- Type systems become tools for correctness, not just bug prevention
- Compilers can be verified mathematical objects
- Programs can carry their own correctness proofs
- Specifications can be executable
As we push toward more dependently-typed languages (Idris, Lean 4, Agda), this correspondence becomes increasingly central to how we think about programming. We're moving toward a world where "It compiles" genuinely means "It's correct."
The dream of verified software—where critical systems are proven correct rather than merely tested—is being realized through Curry-Howard. From verified compilers like CompCert to verified operating systems like seL4 to verified cryptographic implementations, we're seeing the practical power of this theoretical insight.
Perhaps most profoundly, Curry-Howard suggests that the distinction between "writing programs" and "doing mathematics" is illusory. They're the same activity, viewed from different angles. Every programmer is a mathematician; every mathematician, a programmer.
In the words of Philip Wadler: "Proofs are programs; the formula it proves is a type for the program."
Further Reading
- "Propositions as Types" by Philip Wadler - Accessible introduction to Curry-Howard
- "Type Theory and Functional Programming" by Simon Thompson
- "Software Foundations" series - Learn Coq through proving program properties
- "The Little Typer" by Friedman and Christiansen - Dependent types explained gently
- "Programming Language Foundations in Agda" - Modern take on verified programming
The correspondence between types and logic is one of the most beautiful ideas in computer science—a bridge between two worlds that turns out to be a single unified landscape.