The Curry-Howard Isomorphism ============================ NOTE TO SELF: record this class on Zoom Today I want to talk about proofs--specifically, the kind of proofs we've been doing in this course. Let's start with a very simple theorem. For any two natural numbers n and m, there exists a number p such that n + m = p. How do we know that such a p exists? Well, one very straightforward approach is to compute the p by adding n and m. If we assume addition is a built in operation, this is trivial. But, let's assume we are starting at a more basic level, the level where we formalized arithmetic earlier in this course. Now, computing p requires a bit more work, but we can write a computer program that does it. Let's write the program in a functional language. Assume we have a data structure: datatype n = z | s n Now we can write the program as follows: function sum(n1, n2):n { n1 match { z => n2 s m => s sum(m, n2) } } Let's say that to prove our theorem, we want to construct not just p, but also the derivation showing that n + m = p. We can represent derivations that involve sum with another datatype: datatype sum = sum_z n | sum_s sum The first element of the datatype represents a use of our sum-z rule to show that z + n = n, for the argument integer n. The second element of the datatype takes a derivation sum, that states that n1 + n2 = n3, and produces a derivation stating that s n1 + n2 = s n3. We can rewrite our program as follows: function sum_proof(n1, n2):\exists n3.sum n1 n2 n3 { n1 match { z => sum_z n2 s m => sum_s sum_proof(m, n2) } } Notice that this program has the same structure as the program above; we are just producing a representation of the derivation instead of just the final number that n and m sum to. It won't come as a surprise to you that we can also formalize this result in SASyLF. The theorem can be written this way: forall n1 forall n2 exists n1 + n2 = n3. And, here's the proof in SASyLF: proof by induction on n1: case 0 is proof by rule Natural.plus-z end case case S n1' is d: n1' + n2 = n3' by induction hypothesis on n1', n2 proof by rule Natural.plus-s on d end case end induction Compare the proof with the programs we wrote above. Notice that there is a remarkable correspondence between the program and the proof! This is called the Curry-Howard Correspondence: a proof in a particular kind of logic, "Constructive Logic", corresponds to a program in a functional programming language. We can see that case analysis in the proof corresponds to pattern matching in the program. A proof by induction is represented by a recursive function, where uses of the induction hypothesis correspond to recursive calls. Our SASyLF proof is in fact exactly a functional program that takes the "forall" parts of the theorem as arguments and constructs a derivation of the "exists" part of the theorem. The fact that we are doing the proof by constructing a derivation of the result is what makes SASyLF "constructive." The way you prove something in a constructive logic is by constructing an instance of it. Note on terminology: constructive logic is also called "intuitionistic logic" because it came out of a philosophical tradition of mathematics that math does not exist independently of the human mind and is thus a product of "intuition." The idea above generalizes to a lot of logical constructs! Consider a trivial theorem: for all A there exists an A. This could be written as logical impliciation: A=>A. We can prove it with the program: fn x:A => x that just returns its argument. The equivalent SASyLF is: forall d1:A exists A proof by d1. So, implications correspond to functions. What is interesting is that the theorem we proved, A=>A, is related to the type of the function: A -> A. This leads to a second part of the Curry-Howard Correspondence: a theorem in logic corresponds to a programming language type. We can summarize both parts with a table: Languages: Logics: ------------------------ Types <--> Theorems Programs <--> Proofs We can do more! Here's a little theorem: (A /\ B) => A We can prove it with a function: fn (pair: A * B) => first(pair) Here, the logical formula A /\ B corresponds to a pair type A * B. Given a pair of an A and a B, we can extract an A just by selecting the first element of the pair. Using the second function, we can prove the similar theorem (A /\ B) => B: fn (pair: A * B) => second(pair) In SASyLF, we can represent A /\ B as a judgment AandB that has a single "and" rule: A B ----- and AandB The use of "first" or "second" is just doing a case analysis on the input "pair" derivation--there is only one case, and we get a way to name the derivations of the two premises, so we can use either one to prove an A or a B. Proofs involving or are a little bit more interesting. In a constructive logic it's not enough to know that A or B is true...we have to be able to construct evidence of that, and evidence is either A or B. In a program, this means that A or B is represented as a tagged union or sum, which we wrote as A + B. Let's say we have a theorem (A \/ B) /\ (A => C) /\ (B => C) => C We can prove this with the function fn (x:A+B, f: A->C, g: B->C) => case x of inl a => f(a) inr b => g(b) You can see that this function returns a C no matter whether we give it an A or a B for x. In the case of A (where the "left" element of A+B was injected, i.e. x is of the form inl a) we apply function f to get a C. Otherwise, x is of the form inr b and we apply function g to b to get a C. In SASyLF, we can represent A \/ B as a judgment AorB that has two rules, which I've named inl and inr based on the PL theory constructs: A ---- inl AorB B ---- in4 AorB We can reason about an AorB with a case analysis, which elegantly matches the program above. EXERCISE. Write a functional program that proves the following logical assertion (i.e. a program that has a type corresponding to this assertion): (A ˄ B) => (B ˅ A) true is represented by the Unit type in programming languages. It isn't very interesting computationally. false is a little strange from the point of view of functional programming. There is no evidence that can prove false, so in a program false would represent a value you can't possibly compute. A program that does not terminate could be given a void type (this is a "type theory" version of void, not the "void" you have for C or Java functions that simply do not return a value), which would correspond to false. Void can also be written with the \bottom symbol; intuitively, Unit can be written with the \top symbol, which conveniently looks like T (for true). DISCUSSION ---------- Constructive logic is very useful for building proof assistants, because a proof can be represented as a functional program. That is what SASyLF does "under the hood!" Many other popular proof assistants, such as Coq and Agda, work on the same principles. Twelf, the theorem prover whose metatheory SASyLF is based on, is similar, except that proofs are represented as logic programs rather than functional programs. We've seen the literal correspondence between proof in SASyLF and programs in a functional programming language; what about the correspondence between theorems (or "propositions") in SASyLF's logic and types? This is less "obvious", but in fact there is a correspondence there too. SASyLF's logic is pretty sophisticated, so that it can represent statements that are much more interesting than (A /\ (A => B)) => B. In particular, SASyLF logic can express theorems that have variable binding (like first-order logic) and that quantify over expressions with variable binding (a second-order logic). This corresponds to a type system with second-order (or "higher-kinded"), dependent types. The details are beyond the scope of this class, but suffice it to say that there is a type system that corresponds to the logic in SASyLF. Constructive logic is also weaker than the so-called "classical logic" that you may have learned about in a basic logic class. Classical logic includes a negation operator, and a proof principle, the law of the excluded middle: for all propositions P, either P is true or not P is true. Constructive logic lacks this proof rule, so one cannot do proofs by contradiction: assume P is false and show that this assumption leads to being able to prove falsehood, which is a contradiction, so P must be true. This makes sense: constructive logic says that things are true only when you can construct evidence for them, and a proof by contradiction is not positive evidence. Similarly, the law of the excluded middle is not constructive, because you don't know whether P is true or not P is true, and constructive mathematics would require us to know that. Even though constructive logic is weaker than classical logic, there are some counterpoints to this. First of all, constructive logic is useful because it talks about the kinds of proofs that represent computation. Second, even if a surface-level view of constructive logic suggests that it is weaker, in fact, there are ways to represent classical claims in constructive logic, by adding a layer of encoding. So, if you are willing to put up with this indirection, you can get the same power of reasoning. More details can be found on texts on constructive logic.