In the 1930s, the logician Haskell Curry noticed something odd: the rules for combining functions looked exactly like the rules for combining logical propositions. Three decades later, William Howard wrote it down precisely (in a 1969 manuscript, published only in 1980). Programs and proofs are the same mathematical objects viewed from different angles. This is the Curry-Howard correspondence. We are going to build it from scratch.
The lambda calculus#
We need a precise language for talking about functions. The lambda calculus is a minimal notation for defining and applying them, nothing else, invented by Alonzo Church in the 1930s, around the same time Turing was building his machines.
There are exactly three things in it. Variables: , , , names that stand for values. Abstraction: means "the function that takes and returns "; the lambda announces the input and the dot separates it from the body. Application: means "apply to ."
That is the whole language. is the identity function: apply it to some value , substitute for in the body, and get back. takes two arguments, first then , and returns the first. Apply it to and you get , a function that ignores its argument; apply that to and you get . The second argument is discarded.
Types#
So far we have said nothing about what kinds of values these functions expect. The simply typed lambda calculus fixes this by assigning a type to every expression. We write , and read the arrow as "to": give me an , I will give you an . The function has type : give me an , then a , and I will give you back that . The is ignored, but the type still records that it was accepted as input.
Now look at the arrow. As a type, says "give me an , I will produce a ." In logic, says "from , you can conclude ." Same symbol, same meaning. Let's make that precise.
Typing rules#
How do we know has type ? We derive it from rules, each saying: if you know certain things about the parts, you can conclude something about the whole. Here is the context, your list of assumptions.
The variable rule: if is among your assumptions, then . You just look it up.
The abstraction rule: assume the input type, derive the output type, and the lambda packages them together.
The application rule: the function expects an , you gave it an , you get the promised .
To derive , the abstraction rule asks us to show assuming , and that is the variable rule. Two rules, one derivation.
The same rules, in logic#
In natural deduction, implication has the same two rules. Implication introduction says: if, after assuming , you can derive , then you have proven .
Look at that rule, then look at the abstraction rule. The structure is identical. The typing context is the set of assumptions, the type is the proposition , and the term is the proof. The logic rule does not bother naming its proof; it just says a derivation exists. The typing rule tells you what the derivation actually is: the lambda term.
Implication elimination is modus ponens: from and , conclude . That is the application rule with the terms erased. Modus ponens is function application.
Pairs are "and", sums are "or"#
Functions are not the only type. If and , the pair has type ; you need both components to construct it, and you use it by projecting out a component with first or second. In logic this is conjunction: to prove you need a proof of each, and from a proof of you may conclude , or conclude . Pairing is conjunction introduction, and the two projections are the two eliminations.
Sums mirror disjunction. With a value of type , you can inject it into by tagging it left: "I have an , and that is enough for an -or-." A tags right. To use a sum, you case-split: if you can handle the case and the case, you are covered. In logic, from conclude , and to use you argue by cases. Proof by cases is pattern matching.
Currying is a theorem#
Let's trace the full correspondence on a specific example. If you have done any functional programming, you have probably curried a function: turned a function expecting a pair of arguments into one that takes them one at a time. The program is
with type . Take the bundled function and return a function that accepts , then , then feeds them to as a pair.
Read the type as a proposition: if and together imply , then implies that implies . You can always unbundle a joint hypothesis into two sequential ones.
Build the typing derivation. Assume , , and . By the pair rule, has type . By the application rule, has type . Then discharge the assumptions one at a time with three lambdas: first , then , then .
Now read the same tree as a proof. Assume implies . Assume . Assume . From and , conclude : conjunction introduction. Apply modus ponens with the first assumption to get . Discharge , then , then the original hypothesis. The theorem is proved.
Currying is a theorem in propositional logic. The program is the proof.
Reading programs as theorems#
Once you see this, you can read any typed program as a theorem. The identity function proves that every proposition implies itself. The constant function proves that knowing , no additional hypothesis can take it away.
Pause and try: what theorem does function composition prove?
Composition takes from to and from to and returns . Its full type is
Read it as a proposition: if implies , and implies , then implies . Transitivity of implication. The proof is function composition itself: given an , run it through to get a , then through to get a . Two uses of the application rule under three lambdas.
The correspondence, and where it leads#
Types are propositions. Programs are proofs. A function from to is a proof that implies , a pair is a proof of "and", a tagged union is a proof of "or", and type checking is proof verification. Your compiler has been verifying theorems every time you build your code.
One precision matters: the logic on the other side is intuitionistic propositional logic. Every proof is a construction, because every proof is a program. Classical principles like the excluded middle and Peirce's law have no lambda term at all; recovering them takes control operators, as Timothy Griffin showed in 1990.
And this is only the beginning. The simply typed lambda calculus corresponds to propositional logic. Add polymorphism and you get second-order propositional logic. Add dependent types, types that can refer to values, and you get predicate logic. Languages like Coq, Agda, and Lean are built on this; in those languages, writing a program and proving a theorem are the same activity.
Every well-typed program you have ever written was a theorem, with one caveat: in a language with general recursion, a looping program inhabits every type, so the logic is inconsistent. The correspondence carries its full weight in total languages, which is why proof assistants demand that every program terminate.
If you remember three things#
- The lambda calculus has three constructs, and the simply typed version assigns each term a type by three rules: variable, abstraction, and application.
- Those rules are natural deduction with the proofs written in: abstraction is implication introduction, application is modus ponens, pairs are conjunction, sums are disjunction.
- A well-typed program is a proof of its type read as a proposition; currying proves that a joint hypothesis can be split, and dependent types extend the idea all the way to Coq, Agda, and Lean.
Discussion
No comments yet. Be the first.