Six months on, Hughes’s release bot is still running the same code. On a Monday morning Hughes finds that version 2.4 of the library went out over the weekend at the request of a contributor called Reyes, who has no access of any kind. The log says what happened. None of it is a bug in the usual sense. Remember the arrangement: a request to publish carries its own ticket, a value of type MayPublish, and all the bot does is check the ticket’s type before it acts. I praised this as how access control ought to be done, and I still think so. The declarations were
smith :: Maintainer
maintainersWrite :: Maintainer -> WriteAccess
writeMayPublish :: WriteAccess -> MayPublish
so the only way to make a MayPublish was to apply the two policy functions to evidence that somebody is a maintainer. The ticket for Smith’s release of 2.3 was
writeMayPublish (maintainersWrite smith)
and the bot’s job log, which is public, printed it. Reyes copied the ticket out of the log, attached it to a request of his own, and sent it. The bot checked the ticket. It was a MayPublish. It published.
What did the type say? MayPublish is a claim about somebody: they may publish. The evidence inside the ticket was about Smith, and it was good evidence. But the type the bot checked doesn’t say whom it’s about, so to the checker a ticket made for Smith is just a ticket. The checker did refuse Jones, when he wanted to rotate the signing key, because the only evidence in scope for him was a WriteAccess and no function in the policy turns that into a Maintainer. Reyes has no evidence in scope at all. He doesn’t need any. Hand him somebody else’s MayPublish and the compiler has nothing to say. Security people have a name for this, a confused deputy.1 The bot has authority, the credential it was given is genuine, and it used that credential on behalf of the wrong person, because nothing in the credential says whose it is.
Hughes’s first thought is the right one, and I want to watch it fail. The propositional letters stood for whole claims: was “Smith is a maintainer”, was “Smith may publish”. If the trouble is that doesn’t say Smith, write a letter that does. Let be “Smith is a maintainer”, “Smith has write access” and “Smith may publish”. Do the same for Brown and everyone else, and write the policy once per person:
Now a ticket proves , which isn’t , and Reyes’s replay fails. So far so good. But look at what she’s had to write. MAINTAINERS.md lists thirty-seven people, so the policy is now a hundred and eleven letters and seventy-four implications, every one of them a copy of the same two sentences with a different subscript. The same thing written thirty-seven times is wrong in one of them. Worse, the file changes. Somebody joins on Tuesday. The policy now has seventy-six implications, and the proof Hughes checked on Monday was checked against the wrong one. And there’s a third problem, the one the chapter is about. The clerk who checks a proof by comparing symbols can’t see the subscript. To him and are two letters, as unrelated as and were. Nothing in his list of shapes says that the “smith” in the one is the “smith” in the other, and is as well formed as any formula on the page. Hughes has written the name inside the letter, and inside the letter is where the clerk can’t look.
What MAINTAINERS.md actually says is one sentence: every maintainer has write access. Every maintainer, including the one who joins on Tuesday: nothing so far can say that. It keeps coming up. Baker’s runbook said what happens on every payment, and she wrote it for the one on her ticket. A sorting routine promises that every element of its output is at most the next. A sale gives its discount on every day from the start to the end. A bug report said “discount not applied on the last day of the sale”; a patch changed <= to < at both ends; the tests passed; and the discount quietly disappeared from the first day. None of these is a finite list, and the one we tried to write as a finite list has just failed in three ways.
So: how do you say every and some, in a type and in a proof, and what does saying it cost? The language is Frege’s, and in the types the quantifiers turn out to be two things you’ve programmed with for years. Rubbing out the programs, as before, leaves the proof rules, and by then you’ll know the trick well enough to guess them before I give them; I’d like you to try. Pointed at types instead of people, the same quantifier gives generics. The chapter ends where Hughes began, with sentences about things, her policy among them, written down as specifications and proved from. A last section, which you may skip, runs the story the other way round, from a logic to its programs.
4.1 Predicates and quantifiers
The subscript was the right instinct. A letter with a name inside it is two things, a claim with a gap in it and a name, and Hughes’s only mistake was to glue them together where the clerk couldn’t see the join. Pull them apart:
“Smith is a maintainer”, where is a predicate, a claim with a hole in it, and is a name that fills the hole. Now and visibly share a part, and the clerk, who compares symbols, can see that they do. The sentence is still well formed, but it’s now visibly a different sentence from , and a policy that said the first would be seen to say it.
This is Frege’s move, and it’s the one his notation of 1879 was built around. Logic before him analysed a sentence into a subject and a predicate, “Smith” and “is a maintainer”, which is the grammar of Latin; Frege analysed it instead into a function and an argument, and , which is the grammar of mathematics. He thought it the most important thing in the book.2 It matters here because a function can take a variable as its argument. The formula is the policy sentence with the name left out, a claim about whoever is, and it’s one formula, not thirty-seven. On the type side the same move turns Maintainer from a type into a function from people to types, Maintainer smith, Maintainer x, a type with a hole for a name.
Universal and existential quantifiers
A formula with a hole in it isn’t a claim yet, any more than is a number. There are two ways to close the hole, and they’re the two things Hughes was trying to write. The first is to say that the claim holds whoever fills it. For the thirty-seven that would be the conjunction
with one conjunct per person, except that the list won’t be complete on Tuesday and the clerk, as we saw, can’t tell that the conjuncts are alike. So write the conjunct once, hole and all, with a mark in front that says “for every way of filling it”:
That’s for all, and it’s an and with one case for every thing there is, written once with a parameter. The second way to close the hole is to say that something fills it: the review rule, “some maintainer approved this pull request”, is the disjunction
with one disjunct per person, and we write it once:
That’s there is, an or with one case for every thing, and the thing is the tag. Logicians call it the witness. The quantifiers are and with the list of cases made unbounded, and I’ll say so twice more before the chapter is out. Everything else about them (the programs, the proof rules, the side conditions) is the bill for that.
You’ve met both ideas as a programmer. A test suite checks finitely many inputs: it is the long and with its cases written out, Hughes’s thirty-seven letters again, and it says nothing about input thirty-eight. A function says something about every input, because its body is written once, for a parameter, and has to work for whatever arrives: that is for all, in code. And a single failing test settles “some input breaks this” by exhibiting the input: a witness.
Frege’s language, in the notation everyone now uses, comes in three layers: the symbols; the terms built from them, which name things; and the formulas, which make claims.
Definition 4.1 (first-order language). The symbols of a first-order language are of two kinds.
The logical symbols, which are the same in every language: the connectives , and ; the constant ; the quantifiers and ; and a stock of variables .
The non-logical symbols, which make up the signature and change from one language to the next: some constant symbols ; some function symbols ; and some predicate symbols . Each function symbol and each predicate symbol has an arity, the number of arguments it takes.
A predicate symbol of arity is a propositional letter, and is written .
Definition 4.2 (terms). The terms are defined inductively.
Every variable is a term.
Every constant symbol is a term.
If is a function symbol of arity and are terms, then is a term.
Nothing else is a term.
A term with no variables in it is closed.
Definition 4.3 (formulas). The formulas are defined inductively.
is a formula.
If is a predicate symbol of arity and are terms, then is a formula. (When it is a propositional letter , written without brackets.)
If and are formulas, then , and are formulas.
If is a formula and is a variable, then and are formulas.
Nothing else is a formula.
The formulas given by clauses 1 and 2 are atomic.
Three conventions make formulas readable. First, abbreviates . Second, familiar symbols such as and are written between their arguments, so that is the atomic formula . Third, brackets are left out where nothing is lost: the outermost pair goes; and bind more tightly than ; the arrow groups to the right; and and the quantifiers bind most tightly of all. So is , and a quantifier whose scope is meant to be larger is followed by brackets, as in . With the conventions in force, the two definitions fit on two lines:
Remark (looking back: letters). Nothing in the definition replaces anything. A letter of the last two chapters is a predicate symbol of arity , so every formula of those chapters is still a formula, and, once derivations have been defined, every derivation will still be a derivation.
A term names a thing and a formula makes a claim. Keep the two apart, as a parser would by giving them different types. In the symbols and are a constant and a function symbol, is a predicate symbol, and and are variables; is a term and a formula. Writing either where the other belongs would be a type error.
Hughes’s signature, which the chapter uses throughout, has constants for the people and the pull requests, , , , ; a function symbol , the person who opened pull request ; and five predicates:
| Predicate | Reads |
|---|---|
| is a maintainer | |
| has write access | |
| may publish a release | |
| approved pull request | |
| pull request may be released |
People and pull requests are things of one kind as far as the language goes. A variable ranges over both; the predicates say which is which. So is a formula, though not one anybody would assert. (Logics with several sorts of thing, each with its own variables, avoid that at the cost of more bookkeeping.) With these, MAINTAINERS.md is three sentences, which I’ll call :
| What the file says | As a sentence |
|---|---|
| every maintainer has write access | |
| anyone with write access may publish | |
| a maintainer’s approval releases a pull request |
The merge rule as the file states it, “a pull request needs a maintainer’s approval, and its author needs write access”, is . The term names a person without saying who; that is what a function symbol is for. The sentence is the kind a security team writes in English and would rather a machine checked.
Reading quantified sentences
The order of the quantifiers matters, and so does the connective under them. says that everybody approved some pull request, perhaps each a different one; says that one pull request was approved by everybody. The symbols are the same and the claims are very different. The file’s first sentence is . Written with , as , it would say that everyone is a maintainer with write access, which no file says. Under the trap runs the other way. Some maintainer approved is , with ; written with , as , it could be established by anybody known not to be a maintainer, whether or not they approved anything.
Definition 4.4 (free variables). In and the formula is the scope of the quantifier. An occurrence of a variable in a formula is bound if it is the variable of a quantifier or , or lies in the scope of one, and free otherwise. The set of free variables of a formula is defined by recursion on .
; is the set of variables that occur in .
.
.
A formula with no free variables is a sentence.
In , “every number has a larger one”, every occurrence of a variable is bound, and the formula is a sentence. Free and bound occurrences can also mix, even of one variable. In the first two occurrences of are bound and the third is free, and the first is free and the other two are bound. So this is a formula and not a sentence. Everything in is a sentence, which is what makes it a policy rather than a remark about somebody. The merge rule is not: its is free, and it becomes a claim about a particular pull request only when a term is put in for . That operation needs care.
Definition 4.5 (substitution). Let be a term and a variable.
For a term , the term is with every occurrence of replaced by .
The term is substitutable for in a formula if no free occurrence of in lies in the scope of a quantifier on a variable that occurs in .
If is substitutable for in , the formula is defined by recursion on , with standing for any of , and , and for either quantifier:
, ;
;
;
, for a variable other than .
So puts in for the free occurrences of , and only those: clause (c) stops at a quantifier on , because every in its scope is bound. Substitutability is what makes clause (d) safe. If occurred in and occurred free under the , pushing under the quantifier would turn that into a bound one and change what means. The term is not substitutable for in , for this reason; the terms and are. When a term isn’t substitutable, rename the bound variable first, to one that occurs nowhere else. Two formulas that differ only in the names of their bound variables are treated as the same formula, so after renaming every term is substitutable. Ignoring the condition produces a bug that the section on side conditions runs all the way to .
Remark (looking back: substitution in the -calculus). The clauses are those for substitution in -terms in Chapter 3, with a quantifier in the place of a , and the third clause for is the one for an abstraction, . A quantifier binds its variable exactly as a binds its parameter, and renaming before substituting is the same remedy for the same danger.
4.2 Dependent types
What type should maintainersWrite have? In the bot’s old code it was an arrow from Maintainer to WriteAccess, and we’ve seen what that cost. The policy function has to do two things the old type couldn’t say. It has to work for every person, Tuesday’s included. And it has to say, in its result, which person it was given, so that the ticket it makes for Smith is a ticket for Smith. Frege’s move gives the types. With Maintainer x and WriteAccess x as types with a hole in them, Hughes writes
smith : Person
smithMaint : Maintainer smith -- MAINTAINERS.md
maintainersWrite : (x : Person) -> Maintainer x -> WriteAccess x
writeMayPublish : (x : Person) -> WriteAccess x -> MayPublish x
publish : (x : Person) -> MayPublish x -> IO ()
Read the third line slowly, because it’s the only new thing here. The function maintainersWrite takes a person , then evidence that is a maintainer, and returns evidence that has write access. The result type mentions the argument. In the old code the name and the evidence were one value, smith :: Maintainer, and that was the bug. Here they’re two: the person, and a separate value smithMaint that’s about him, whose type has the person in it. So the type of the function is an arrow whose right-hand side depends on what went in on the left. None of the type systems so far can write that.3
On the program side, the long and is a record with one field per maintainer: thirty-seven tickets, one for each person. A function from maintainers to tickets holds the same information (hand it Smith and it returns Smith’s field), and when you don’t know how many fields there will be, a function is the only way to write the record down. So a proof of for all is a function from things. The long or is a thirty-seven-way Either: a value with a tag saying which case it is. Here the tags are people, so the value is a pair of a person and evidence about that person. So a proof of there is is a pair of a thing and evidence about it.
One change of notation comes with the types. Logic writes , , and ; type theory writes , , and for the same four things. This section uses the type theorist’s letters, the next goes back to the logician’s, and the table at the end of the section lines them up.
Definition 4.6 (dependent types, terms and contexts). Fix a signature. Its terms name things, and its variables stand for things. A second stock of variables stands for evidence. The types are the formulas over the signature, written as a type theorist writes them:
where is the pair type, the sum type, the dependent function type and the dependent pair type, with ranging over the one kind of thing the signature talks about; negation is . The terms are
where and are evidence variables, is a variable for things and is a term of the signature. The first three lines are the propositional terms; the fourth has the four new forms, an abstraction over a thing, an application to a thing, a pair of a thing and evidence, and the match that opens such a pair. A context is a finite list of entries of two kinds: a variable on its own, which declares a thing, and a typing , which declares evidence. No variable is declared twice, and every variable free in a type in is declared earlier in than the entry that mentions it. A term of the signature is in scope in if every variable in is declared in .
A declared variable is a thing nobody has chosen yet, a parameter, and the four new rules all turn on it. Here is the whole system. The rules for , , and are the propositional ones, except that a context may now contain declarations.
Definition 4.7 (typing rules). The judgement is defined by the rules below. In every rule, each context written must be a context in the sense just defined; in (abs), for example, every variable free in must be declared in .
Read (E) first, because it’s a function call. You have a program that works for every and you call it on the in hand. The type of the result is the body with put in for : substitution. And has to be in scope, because a program can mention only declared names. (I) is what happens when a definition is finished. You typed the body with declared and nothing said about it, so the program works for every , and you may say so. Proved for an arbitrary , hence for all. (I) is the constructor: supply a thing and evidence about it, and out comes a value whose type mentions neither. (E) is the pattern match. Open the pair, giving names and to the thing and the evidence, and go on to produce something. The something may use and , but its type may not mention , because was a name you made up for the duration of the match. Compare the rules for and , which are these four with the list of cases cut down to two: a pair with two fixed fields instead of a function from things, a tag from instead of a thing. That’s why the letters are and : a function from things is a product with one factor per thing, and a pair with a tag is a member of a sum with one summand per thing, just as is a sum with one term for each .
Side conditions and the context
Logic textbooks give the rule for for all a proviso: must not occur free in any hypothesis. The proviso is needed; without it the rules prove , as an exercise at the end of the chapter shows. Yet (I) above carries no proviso, because the context already enforces it. In the variable is the last thing declared. By the definition of a context, an entry may mention only variables declared before it, so no in can say anything about . If you want to generalise over while some hypothesis about is still in force, the rule doesn’t apply, because isn’t last. You have to discharge every hypothesis that mentions first, with a each, and only then does come to the top. A context is a stack, and you can only pop when everything pushed after it has gone.4
The one condition the stack can’t enforce is the one written on (E). The type of the result isn’t in the context, so nothing stops it mentioning unless the rule says so. That is why (E) is the only rule with a freshness condition; the conditions on (E) and (I) only ask that use names already declared. It is also the condition a module system enforces: the part of a language that lets a library hide how it is built.
Now some typings, as sequent-style trees, in which every node is a whole judgement with its context. Hughes’s declarations become one context. The people are constants of the signature, which are always in scope, so they need no declaring, and what is left is the evidence: the three policy functions ( and for the two above, and for the review rule), publish, and for smithMaint. With the types written in the predicates of Hughes’s signature, that context is
It stays the same in every tree below; whatever a rule adds to it is written out in full.
Example 4.8 (Smith’s ticket). Smith’s ticket is built in two stages. First the write-access ticket, from the first policy function and the file’s evidence:
and then the publishing ticket, from the second policy function and the tree just drawn, which I write as dots: Each (E) bar picks the person the policy sentence is being used for. The program at the root is the ticket, and its type now has Smith’s name in it.Example 4.9 (Reyes’s replay). Reyes’s request handler passes the ticket from the log to publish,5 and the checker gets as far as the ticket:
reyes
Here is the term at the root of Smith’s tree, , copied from the log. The confused deputy is a type error, and the error says whose ticket it was. Nothing in the bot’s code had to be clever.
Remark (looking back: Jones). The tree for Smith’s ticket is the typing of the same ticket in Chapter 2, with the two (E) bars added. And Reyes is refused for the reason Jones was refused there, when he asked to rotate the signing key: a missing piece of evidence. What has changed is that the type can now say whom the evidence is about.
Example 4.10 (every maintainer may publish). The policy composed and then generalised is the typing that answers Hughes’s original question: may every maintainer publish? In the context , with declared and then a hypothesis about it, two (E)s and two (app)s give , by the trees of Example 4.8 with for and for . Then two bars close it, and the order they come in is the side condition:
The (abs) bar pops , the hypothesis about . Then is on top and (I) pops it. Try them the other way round and the (I) bar has nothing to apply to, because isn’t the last entry. The program is function composition with a person threaded through it, and its type, read as a sentence, is every maintainer may publish, which isn’t in the policy but follows from it.
Example 4.11 (the review rule). Brown approves the pull request, and the bot records
by (I) from and : the approver and the evidence, packed together, in a value whose type says only that somebody approved. To release, the bot needs , and the third policy sentence gives it, once the pair is opened. The bot holds the record as , so its context is , where is short for the type . Opening the pair adds two entries, a person and the evidence about them, and in that context the policy applies. Two (E)s and an (app) give
which I write as dots in the tree that opens the pair: The right-hand branch calls the policy at and and applies it to , where is a name for whoever it was. The side condition holds because doesn’t mention . Suppose instead that the handler tried to conclude, from the approval record, that the approver is a maintainer:Inside the match, is perfectly good. Outside it there’s no . It was a name the match made up, and a type that mentions it is a type that mentions nothing. “Some maintainer approved; call them ; so is a maintainer” isn’t a step the rules allow, and the rule says why in four words.
Reduction
Running a program means rewriting an introduction met by its own elimination, and the quantifiers add two such steps, with the shapes of and of a case on a tag.
Definition 4.12 (reduction). A step rewrites, anywhere inside a term, an introduction met by its elimination:
There is no step for , because has no introduction form for it to meet. A term in which no step can be taken is in normal form.
The last two are the new ones. Call a function on a thing and the thing goes in for the parameter. Open a pair that was packed and the thing and the evidence go in for the two names. The table of connectives and types gains two rows:
| Connective | Type | Introduction (constructor) | Elimination (pattern match) |
|---|---|---|---|
| application | |||
| , | |||
| , | case with two branches | ||
| — | |||
| application | |||
| application | |||
let with two names |
A function from things, and a pair of a thing and evidence: that’s what Hughes’s bot checks. The first two columns are a dictionary between two notations: for , for , for and for , with and the same on both sides.
Types that mention values weren’t invented for Hughes. In 1967 Nicolaas de Bruijn, a mathematician at Eindhoven, set out to have a machine check a whole mathematics textbook, and found that a checker couldn’t do it unless the types were allowed to mention the things the theorems were about. The language he built for the job, Automath, has the rules above, and in 1977 one of his students fed it the whole of Landau’s Foundations of Analysis, every lemma, and it checked.6 Most proof assistants built on dependent types count it as an ancestor.
Remark (looking back: general recursion). The remark on general recursion at the end of Chapter 3 said that a programmer who wants both recursion and a consistent logic must say in the type where the recursion is allowed to go, and that this takes a logic with more to say than the propositional one. Types that may quantify over things are that logic.
4.3 First-order natural deduction
Twice now a typing has had its programs rubbed out to leave a logic: Hilbert’s proofs under the combinators, and Gentzen’s under , pairs and tags. The Curry–Howard correspondence says this always works. Beside every type system there’s a logic, and the logic is what the typing rules say once the terms are deleted. We’ve just built a type system, so there’s a logic standing beside it.
Before you look for it, one thing changes on the page, and it’s the letters. Peirce, who found the quantifiers at about the same time as Frege and without knowing of him, wrote them as and , for the reason given in the first section: a long and is a product and a long or a sum (Peirce, 1885). Logic traded his letters for and , and nearly every logic book since has used those. Type theory took them back: ninety years after Peirce, Martin-Löf wrote the two dependent types as and , for the reason given with the typing rules above.7 So rubbing out the programs means renaming as well as deleting: becomes and becomes , along with to and to . The logic is written in the logic’s letters from here on, and before I write it down I’d like you to find it.
Problem 4.13. Take the four rules for and , delete every program, and rename the two types, leaving the contexts and the formulas. What are the four rules of proof that remain, and where have you heard each of them said aloud?
Here’s what’s left, with the declarations kept, since a declaration is a name and not evidence:
with the side conditions carried over unchanged ( in scope in the second and third, not free in in the fourth), and here’s where you’ve heard them. Let be arbitrary: the first. In particular, for : the second. Take to be : the third. Let be such a thing; then: the fourth. You’ve used all four as a programmer. A function body, written for its parameter without knowing which value will arrive, is let be arbitrary. Calling the function on the value in hand is in particular, for . A failing test is take to be : one input, exhibited, settles “some input breaks this”. And code that receives “some maintainer approved” and works with whoever it was, without being told who, is let be such a thing.
Gentzen had the four rules in 1934. Natural deduction was first-order from the day it was invented, and the propositional rules are the half of his system without quantifiers.8 Here is the whole system as trees.
Definition 4.14 (derivations, with quantifiers). The derivations are defined inductively, and with each derivation we define at the same time its conclusion, the formula at the root, and its set of open hypotheses , a finite set of formulas. In the clauses below a derivation is drawn as a name with its conclusion written underneath it, and a formula in square brackets above a derivation is one that the clause discharges. Throughout, is a term, is a variable, and substitution is as defined earlier in the chapter.
(hypothesis) For every formula , the one-node tree whose only node is labelled is a derivation, with conclusion and .
(I) If has conclusion , then for every formula ,
(E) If has conclusion and has conclusion , then
(I) If concludes and concludes , then
(E) If concludes , then
are derivations, each with .
(I) If concludes , then for every formula the tree with root and immediate subtree is a derivation, labelled I1, and symmetrically from a derivation of , labelled I2; in both cases .
(E) If concludes , and and both conclude , then
is a derivation, with .
(E) If concludes , then for every formula the tree with root and immediate subtree is a derivation, with .
(E) If concludes and is substitutable for in , then
(I) If concludes , where is substitutable for in , and is free neither in nor in any formula of , then
(I) If concludes and is substitutable for in , then
(E) If concludes and concludes , where is substitutable for in , and is free neither in , nor in , nor in any formula of , then
is a derivation, with .
Nothing else is a derivation.
We write , and say that is derivable from , when some derivation has conclusion and ; such a derivation witnesses . The variable of (I) and (E) is the eigenvariable of the step.
A tree has no declarations. It records what a context records by computing , and it records the stack in the same way. Where the type system said is the last thing declared, the (I) clause says two things: is free in no open hypothesis, which is the same fact read off the leaves; and is not free in , which a fresh declaration guarantees. The (E) clause states the one condition the type system had to write, not free in , and two that the context gave for free: is fresh for and for the other open hypotheses.
Set the two clauses beside the turnstile, writing for the open hypotheses of the tree above the bar, and the correspondence is visible bar for bar. On the left is the tree; on the right, what it witnesses:
with free in no formula of , nor in ; and
with free in none of , and the formulas of . Read on the right, the conditions are the erased rules with the declaration turned into a test: where the context declared last, the turnstile asks that nothing to the left of it mention .
Theorem 4.15 (Curry–Howard, with quantifiers). The correspondence of Theorem 3.9 extends to the quantifiers. Erasing the terms from a typing, and writing , , and for , , and , turns it into a derivation, with (I), (E), (I) and (E) becoming (I), (E), (I) and (E); and decorating a derivation with terms turns it into a typing. Writing for the set of types of the evidence entries of , read as formulas,
for every context that declares the variables free in .
Erasure treats the two kinds of variable differently, and that is the difference between quantifiers and connectives. Erasing deletes , since a variable standing for evidence has nothing to say once the evidence is gone. Erasing doesn’t delete , though, because a declared thing isn’t evidence but a name, and the formula still needs it. So a logic keeps its variables when it loses its programs, and derivations with quantifiers have variables in them, where propositional derivations had none.
Normalisation and consistency
The theorems about running programs carry over, each with two more cases. A lemma proved for an arbitrary and then used at a particular is a detour, (I) met by (E), and it is removed by the step . A witness packed and at once unpacked is (I) met by (E), and it is removed by the step for .
Theorem 4.16 (subject reduction). If and , then .
In words: running a program never changes its type.
Theorem 4.17 (normalisation). Every typed term runs to a normal form. Equivalently, every derivation can be rewritten into one without detours (Prawitz, 1965).
Proof for terms without and . The counting proof of Theorem 3.12 goes through with one more kind of redex. Give an atomic type degree 1 and degree one more than . Putting a term in for changes no type’s shape, so has the degree of . The observation still holds for the new step . What it substitutes is a term of the signature, which names a thing and is never an introduction form, so the substitution creates no redex. And if lands under an elimination and is an introduction form, the new redex has the degree of , less than that of , just as the body of a step does. The rest of the count is word for word. □
breaks the count as does, and for the same reason: is an whose tags are things. A step on a puts its body where the stood, and has the result type , which can have any degree at all. If the stood under an elimination, the new redex can have a larger degree than the one removed. Prawitz’s proof covers every connective and both quantifiers, with the permutative conversions added; Tait’s method of computable terms extends to the quantifiers too. For terms with or we take the theorem on trust.
Proposition 4.18 (closed normal forms). A closed typed term in normal form is an introduction form: , , , , or .
Theorem 4.19 (consistency). In first-order natural deduction, no atomic formula is derivable from no hypotheses: , and .
Corollary 4.20 (disjunction property). If , then or .
Corollary 4.21 (existence property). If , then there is a term with .
The last three follow from the first three as Theorem 3.15 did. No introduction form has type or an atomic type, so there is no closed normal term of either. A closed normal term of type is or , and its tag says which disjunct holds. A closed normal term of type is with , and its first half is the witness.9
4.4 Side conditions
Three things can go wrong with a quantifier rule, and each of them is a bug you’ve met in a program. The examples of the section on dependent types refused two of them already; here all three are set side by side, in both notations. You remember a side condition once you’ve watched its absence prove everything, so the third bug is run all the way to below; one of the exercises does the same for the first.
Generalising over a fixed variable
Suppose Hughes’s bot is handling a request from some user , and the hypothesis in force is : this requester is a maintainer. One application of (I) would give , everyone is a maintainer, from which, with the policy, everyone may publish. Both notations refuse it, and they refuse it for one reason. In the type system, was declared before , so it isn’t on top of the stack and (I) doesn’t apply. In a tree, is free in the open hypothesis , and the (I) clause says no. Discharge the hypothesis first, by (I), and generalising becomes legal. But what you’ve generalised then is , and that is harmless.
Of the two, the tree says it more sharply. The context asks that be free in no hypothesis available; the tree asks that be free in no hypothesis used. A hypothesis about lying in the context untouched blocks (I), but it doesn’t block (I), since it isn’t open. Nothing provable changes, by the Curry–Howard theorem. But the tree states the condition that matters and nothing more, and that is one reason proof theory is written in trees.
Escaping witnesses
“Some maintainer approved the pull request; call them ; so is a maintainer.” We saw the type system refuse this in Example 4.11, with the result type , and the tree refuses it by the first condition of the (E) clause, that not be free in the conclusion. The other two conditions rule out two more ways of cheating. You might choose as the fresh name a variable that already mentions, or one that some other open hypothesis already says something about. Either way “call them ” would be naming a thing that was already in play. Together the three say that you may use the thing and may not learn which thing it is, which is what a module boundary says.
Variable capture
The third bug is the reason (E) asks that be substitutable. Drop that condition and the system proves from two hypotheses any programmer would accept of the natural numbers:
“every number has a larger one” and “no number is less than itself”. Take to be the variable , which is not substitutable for in , since it would land under the , and pretend (E) didn’t care. The left-hand bar below is that instance, , and it’s the illegal one.
Four bars, and is the two harmless hypotheses, so the tree would witness . The system has proved from two facts about the natural numbers. The only thing that let it happen is the left-hand bar. (The (E) at the root is legal: is free in none of , and .) You can see what went wrong in the formulas. The we substituted meant “some given number”. On landing under it came to mean “whichever the quantifier picks”. So says that some number is less than itself, which the hypothesis never said. With the condition in force the left-hand bar isn’t available. The fix is to rename first: the hypothesis is equally well written , its legal instance at is , and nothing follows from that but what should.
Remark (looking back: capture in the -calculus). Chapter 3 promised this bug on the programming side, where substitution renames a bound variable before passing under a that would capture. Running a program is substitution, , and it needs the same condition. Take , the combinator that keeps its first argument (written in that chapter’s letters), apply it to a free variable , and run it blindly:
The free has landed under the inner and been captured. The result is the identity, which returns whatever it’s given. The program’s meaning has changed. Rename first, , and the constant function we meant comes out. On the proof side, with the free of type and the parameter of type , the program before the step is a proof of from the hypothesis , and the blind result is a proof of from nothing. Capture has swapped one theorem for another, which is what it did on the logic side too. One bug, one fix, on both sides of the correspondence.
A compiler or interpreter doesn’t substitute text when it calls a function; it carries the argument’s value in an environment. Macros and refactoring tools do substitute text, and that is where you’ve met the bug. The logic side has been known for a century, and every textbook states the condition. What the correspondence adds is that it’s the same condition.
4.5 Generics
The quantifier so far ranges over things. Let it range over types instead and you get something you’ve been writing for years. A type variable stands for a type nobody has chosen yet, and is the type of a program that works for every type . Such a program is a type abstraction : it takes a type and returns a program. You use it by an instantiation , which hands it a type . The rules are (I) and (E) of dependent types with types in the place of things,
where may mention only type variables declared in . The context is the same stack, and running is one more step, .
Example 4.22 (the identity). The function id x = x is : declare , declare , pop with (abs), pop with (I). Using it at Int is one (E),
and Haskell inserts that instantiation for you at every call.
The type of length is [a] -> Int, and the a in it is a parameter, a type nobody has chosen yet. Haskell leaves the quantifier out, and writes it forall only when asked. Written in, it reads
so length is a function that works for every element type at once. Without a way to say for every you’d have three options. The first is one copy per element type,
lengthOfIntList, lengthOfStringList, lengthOfBoolList, …
which is Hughes’s thirty-seven letters again. The second is to give it the type [Object] -> Int and cast on the way in and out. Java made people do that until 2004, and a wrong cast is caught only when the program runs. The third is to turn the checker off for that part of the program. With the quantifier you write the code once and check it once, for every type, and that’s why map, filter, sort and every container you’ve used have one in their types. The logic got there first: a generic function is a proof of a schema, written once.
Remark (looking back: schemas). Chapter 1 typed by a schema, , and recorded in square brackets after (K) the types chosen for and at each use. The brackets are instantiations: is the program , and the typing “(K) ” is the term . Java’s static <A, B> declares the two type variables in the same place.
The side condition on (I) turns up as an error message. Try
f :: a -> b
f x = x
and GHC answers couldn’t match expected type b with actual type a, adding that a is a rigid type variable bound by the signature. Here’s what happened in the stack. The signature declared and and promised a program that works for both separately. The body can only be typed if and are the same, which means a hypothesis about is still in force when the checker reaches the (I) that would pop it. Rigid is the compiler’s word for declared and not last, a variable the context has fixed and you may not generalise over.
Example 4.23 (a counter). There is a over types too, and it’s a module. Suppose you want a counter: a thing with a starting state, a way to step it, and a way to read off a number. You don’t want to say what the state is, because that’s the implementation’s business.
{-# LANGUAGE ExistentialQuantification #-}
data Counter = forall a. MkCounter a (a -> a) (a -> Int)
intCounter = MkCounter (0 :: Int) (+1) id
listCounter = MkCounter ([] :: [()]) (():) length
tick3 :: Counter -> Int
tick3 (MkCounter s step out) = out (step (step (step s)))
Both counters are values of the one type Counter, though one carries an Int and the other a list, and tick3 returns 3 for each without being told which. The type Counter is , there is a type with these three operations. MkCounter packs a witness, the state type, as (I) packed a thing. The pattern match in tick3 opens the pair, as (E) did, and its result type Int doesn’t mention the state type. A function that tried to return the counter’s state would have result type , and GHC says that the rigid type variable a would escape its scope. It’s the condition written on (E), in the compiler’s words. The implementation type may be used inside the match and may not leave it. A module boundary is a boundary because of this. It’s one side condition on one rule, not a convention anybody has to remember.10
The generic quantifier, side conditions and all, has been at work every time the compiler has read a definition with a type variable in it. The domain was types rather than people, though, which is why the type never amounted to a specification. Specifications need the first-order quantifier, over things.
4.6 Specifications
Read the consistency theorem with names in the language. The logic proves no atomic sentence: neither nor nor . Whatever it proves outright has a connective or a quantifier on the outside and is about how claims relate. Every fact about a particular person, element or day has to come from the hypotheses. A specification is a set of sentences taken as hypotheses, and what we prove, we prove from it. Three specifications, then, in increasing order of interest.
Example 4.24 (the release policy). From and we derive by the tree for Smith’s ticket with its programs rubbed out: two (E)s and two (E)s, with the two policy sentences and the one fact. So the tree witnesses , and it is a certificate. An auditor who disbelieves the answer needn’t rerun the bot or understand it; they check four bars. And since the logic proves no atomic sentence on its own, the auditor knows that came from MAINTAINERS.md and from nowhere else. From alone we can derive every maintainer may publish, which no policy engine settles by evaluation because it quantifies over everyone at once. It comes in two pieces, to fit the page. Call the first :
and then
After the discharge, is the two policy sentences, and they’re sentences, so is free in neither and the last bar is legal. The tree witnesses . Had we generalised before discharging, would still have been open with free in it, and the clause wouldn’t have applied. In plain words, we’d have been claiming the policy for all users on the strength of one. The tree is the typing of the same sentence in Example 4.10, with the programs gone, bar for bar.
Example 4.25 (a comparator). Here’s insertion sort, written so that the comparison is a parameter. Standard libraries write their sorting routines this way, because a library can’t know what you’re sorting.
sort(lt, xs):
out = []
for x in xs:
i = 0
while i < length(out) and lt(out[i], x):
i = i + 1
insert x into out at position i
return out
Everything so far applies to that program. It has a type, and the type is polymorphic, like that of length,
one program for every element type, instantiated at each call. The checker proves that type, and by the Curry–Howard correspondence the proof is the program. So the compiler has certified something real. This code won’t add a string to a number, won’t call a non-function, and will hand back a list of the type it was given. About sorting it has certified nothing whatever.
The gap is the parameter lt. Give sort a comparison that returns True when its two arguments are equal: <= where < was meant, about the commonest mistake there is. The type doesn’t change, because <= and < have the same type. The program compiles, runs and returns a list, which is sometimes not sorted. A library sort, given a longer input, may do something stranger. This is an exception Java’s sort really throws:
java.lang.IllegalArgumentException:
Comparison method violates its general contract!
Read it carefully, because it’s unusual and exact. The sort hasn’t checked the contract. Checking it would need every pair of elements, which costs more than the sort. What it noticed is that its own bookkeeping had become impossible: it merged two runs it had recorded as ordered and found them not ordered, which can’t happen if the comparison behaves. It detected the failure of a consequence of the contract. To say what that means we have to be able to state the contract and derive the consequence, and now we can.
What sort needs of lt is that it be a strict weak order, the kind of order that may leave two different elements tied, as sorting people by surname does: three sentences, with for lt(x,y).
| What the library needs | As a sentence |
|---|---|
| nothing is less than itself | |
| if and then | |
| if , every is above or below |
Call those three . They say for all, and they’re about the elements, so no propositional type could hold them, or even write them down. They’re what the library’s documentation is trying to say in English. What a comparator built from <= violates is asymmetry. It reports and for a pair of equal elements, and no comparator obeying the contract can, because
That isn’t one of the three sentences, so it has to be derived. Informally the argument is one line: if and then by transitivity , which irreflexivity forbids. The derivation makes each of those words a step. Here it is as a tree, in two pieces to fit the page. The first piece, which I’ll call , takes transitivity down to on the supposition:
The second uses irreflexivity, discharges the supposition and generalises:
Read it from the top. The three (E) bars of strip the quantifiers off transitivity, choosing , and ; the last choice is the step that says “then ”. The (E) bar fires the result on the supposition. In the second piece, irreflexivity at meets and gives . The (I) bar discharges the supposition, turning “the supposition leads to absurdity” into “not the supposition”, and the two (I) bars generalise over and then over . The order is forced. The (I) bar for can’t come above the discharge, because is free in the supposition while it is open. This is the stack again, and it is why the proof couldn’t have generalised first and supposed afterwards. When the last bar is drawn, is irreflexivity and transitivity, two of the three sentences of , so the two pieces together witness the claim, .
Now decorate it. The program is one line:
with and two of the sentences of as variables, and the type written in the type theorist’s letters. Given two elements and a claimed pair of comparisons , it calls transitivity at , , , hands it , and hands what comes back, a proof of , to irreflexivity, which returns . Nine bars of proof, one line of program, and you can see in the program why the proof had the shape it had: three s, one for each let … be arbitrary and one for the supposition, and the quantified hypotheses called as functions. So a comparator obeying the contract can never report both and , and when the sort catches one doing just that, the exception is reporting a theorem being violated. The type checker couldn’t state that theorem. A verifier, which is a type checker whose types may quantify over things, can state it and check the proof just given. That is the distance between safe and correct, on a program you’ve called a hundred times.
Example 4.26 (the sale). And the patch. With for “the discount applies on day ”, what a sale is comes to one sentence,
The patched code applies the discount only when and , so it fails the sentence at , where the sentence demands a discount and the code withholds one. Nobody had written the sentence down, which is how the patch got approved. The bug report said “last day”, the tests said “last day”, and the only thing that said “every day” was in the head of whoever wrote the sale. Once the sentence is written, “is the patch correct?” has an answer a checker can verify, and the answer is no. What’s still missing is a logic in which “after this patch runs, the discount applies” is itself a formula. Chapter 13 has one, and comes back to this patch.
Witnesses
The quantifier is the whose tags are things, and by the existence property a proof of it carries its tag. “Is this file readable by anyone outside the team?” is an existence question, and a yes that can’t say who is no use to a security engineer. In this logic a yes proved from no hypotheses comes with the name attached.
The insistence on a name is also worth distrusting. “Either the program halts or it doesn’t” is the kind of thing you’d like to be able to assert without producing a halting run or a proof that there’s none. “Some input breaks this function” is the kind of thing you’d like to assert without producing the input. In this logic you may assert neither, because a proof of or says which and a proof of there is says what. Whether that’s a virtue or a restriction is a question for Part 2.
4.7 Second-order logic and System F (optional)
This section may be skipped: nothing later in the notes depends on it. The section on generics met a quantifier over types from the programming side. Here it is found the other way round: start from a logic, decorate its derivations, and see what programs come out. On the way, , and turn out to be definable from and , and a quantifier over all propositions turns out to need a consistency proof of its own.
The first-order quantifiers range over things: people, pull requests, days. Look back at the propositional theorems, though, and you’ll find a quantifier of another kind that nobody wrote down. The theorem was never about in particular. Its proof works with any formula at all in the place of , and that is why the Hilbert system states its axioms as schemas, with Greek letters standing for every formula. A schema is a claim about all formulas made one instance at a time, which is Hughes’s thirty-seven letters once more. The cure is the same as before. Let a quantifier range over propositions, and write the claim once:
“whatever proposition is, implies ”.
The variable needs a symbol of its own. The propositional letters are constants: each stands for one fixed claim, and nothing can bind it. A variable for propositions is as different from a letter as the variable is from the constant , so we write propositional variables differently, as .
Definition 4.27 (second-order propositional logic). Fix the propositional letters and, separately, a stock of propositional variables . The formulas are
where binds in . Free propositional variables, substitutability, and the substitution of a formula for the free occurrences of in are defined by the clauses for first-order formulas. The derivations are built by the clauses for hypotheses, (I) and (E), and two more:
each with the open hypotheses of .
The two quantifier rules are the first-order ones with propositions in the place of things: (I) generalises over a proposition nothing open says anything about, and (E) uses the result at a particular one.
Example 4.28 (two schemas, as theorems). Two schemas become single theorems: the identity, and Frege’s first axiom, schema (I) of the Hilbert system.
In each tree the (I) bars come after every hypothesis has been discharged, so nothing open mentions or and the bars are legal. Both trees have , so they witness and . An instance of the schema is now one step: (E) at turns into .
The connectives, defined
The logic has and and nothing else, and needs nothing else, because , and can be written with those two. Say what you can do with a conjunction: from you can conclude every claim that follows from and together. Then let that be the definition.
Proposition 4.29 (the connectives in second-order logic). Define, with a propositional variable not free in or ,
With these definitions every rule of natural deduction for , and is derivable: for each rule, its conclusion is derivable from its premisses, discharging what the rule discharges.
Proof. Write for the formulas the premisses are derived from, and for the conclusion wanted. The variable is chosen free in no formula of , so every (I) below is legal. (E): from , (E) at gives . (I): from and , two (E)s give ; (I) gives , and (I) gives . (E1): from , (E) at gives ; and , as in Example 4.28; so (E) gives . (E2) is the same with and . (I1): from , (E) gives ; two (I)s and (I) give . (I2) is the same from . (E): from , (E) at gives ; the two branches are and , and (I) turns them into and ; two (E)s give . □
Each connective is defined by its elimination rule. says “whatever follows from and together, follows”; says “whatever follows from and also from , follows”, which is proof by cases; and says “everything follows”. So propositional natural deduction sits inside this one, built from and alone.
Higher-order logic
Second-order propositional logic quantifies over propositions. Higher-order logic goes the whole way: over properties of things, relations between them, properties of properties, and functions of every type. Its language is the simply typed -calculus itself, with a type of propositions and a type of things, so that a property of things is a term of type , and a variable of that type can be bound by . This is Church’s simple theory of types (Church, 1940), the logic for which he typed his calculus. In it, induction on the natural numbers is one sentence,
where first-order logic needs a schema, one axiom for each formula. The power has a price: Chapter 15 shows that once the quantifiers are read as ranging over all properties, no proof system whose proofs can be checked mechanically captures that reading, even for second-order logic.
Impredicativity
Look at (E) once more. The formula put in for may be any formula at all, including itself: may be used at , and , which is , at . A quantifier over all propositions ranges over a collection that includes the proposition it is helping to form. Definitions of that kind are called impredicative, and Russell banned them from Principia for fear of the self-application that produced the paradoxes. So we have to ask whether this logic is consistent. For first-order logic the answer came from programs: decorate the derivations, run them, and look at what they stop at. Do the same here.
The polymorphic -calculus
Decorate a second-order derivation as propositional ones were decorated. The bars for become and application, as before. A (I) bar over becomes a type abstraction , a program that takes a type and returns a program, and a (E) bar at becomes an instantiation , which hands it one. The formulas become types, with the renaming run backwards: a propositional variable becomes a type variable , and becomes , the of dependent types with types in the place of things. The two theorems of Example 4.28 decorate to
Here is the calculus that results, the one the section on generics used informally.
Definition 4.30 (polymorphic -calculus). Fix a stock of type variables . The polymorphic types and the terms are
where is an evidence variable. In and the type variable is bound, and substitutes the type for the free occurrences of in by the clauses for substitution in formulas. A context is a finite list of declarations of type variables and of typings ; no variable is declared twice, and every type variable free in a type in the context is declared earlier than the typing that mentions it. The rules are
and the steps are and .
The rules for are those of dependent types with the domain changed from things to types, and the context is the same stack. There are no pairs, no unions and no , for the reason the logic has already given: they can be defined. Erasure and decoration go through as before, so a formula without letters is derivable from no hypotheses exactly when a closed program of the corresponding type exists. (The letters are the logic’s constants, and the calculus has no counterpart for them, just as it has no types like Int built in.)
Data types, defined
Decorate the derivations in the proof of the proposition on the connectives, and the definitions of , and become programs, with for and for the result type:
They also run as they should:
and a on runs to in the same way. So pairs, unions and the empty type come free with functions and . The same trick defines the data types you know. The booleans and the natural numbers are
with and , and the numeral with copies of . Addition is
do steps, then more.
Remark (looking back: booleans and numerals). These are the booleans and numerals of Chapter 1, where was , was , and applied to times. That chapter never gave the numerals a type. In the simply typed calculus each of them has the type for one chosen in advance, and then exponentiation, which uses one numeral at the type of another, has no type that takes two numerals of the same type to a third. The polymorphic type lets every use choose its own:
computes . And the two projections that define , and , are and again.
Existential types, defined
The counter of the section on generics had the type . In the polymorphic calculus over types is defined like , by what you can do with it:
“whatever follows from , whatever is, follows”. The pattern match in tick3 opens the pair at the result type Int: instantiate at Int and hand it . That substitution puts Int under the , so it is legal only because Int doesn’t mention , which is the escape condition once more.
Normalisation
Now the question the logic left open. Every program of the calculus is a decorated second-order derivation, impredicative instantiations and all, so if the programs all stop, the old argument goes through.
Theorem 4.31 (normalisation for the polymorphic -calculus; on loan). Every typed term of the polymorphic -calculus runs to a normal form.
Girard proved it in 1971, by extending Tait’s method of computable terms. It is much harder than the normalisation theorems it extends.11 The shape of closed normal forms is as before, and so is the conclusion: there is no closed term of type , and second-order logic doesn’t prove . Impredicativity is safe here. The same argument says more. A closed normal term of type is , with in the context . The type has no introduction form, so is neutral (a variable with eliminations applied to it), with head ; and nothing can be eliminated from , so is . The only program of type is the identity. The type says what the program does, without your having to read the program. Reynolds called this parametricity, and Wadler turned it into a slogan, theorems for free (Wadler, 1989).
The language is also very strong. Every function it can type from to stops on every input, so it has no , but the functions it can type are exactly those that second-order arithmetic can prove total, which is far more than any programmer has needed.12
There is a limit. It is the old story of a collection that contains itself. In the polymorphic -calculus a type quantifies over types, but the collection it ranges over is not itself a type, so no program can be handed the collection of all types as a value. Martin-Löf’s first type theory, of 1971, took the last step: a type of all types, with . Girard showed that it proves everything. There is a closed term of every type, and it never stops, built, like the Monster and Russell’s paradox before it, from a collection that contains itself.13 The repair, in Martin-Löf’s later theories, is a hierarchy of universes, , none of them a member of itself.
4.8 History
The quantifier was invented twice, like natural deduction. Frege had it in 1879, in a notation nobody else used. Peirce and his student Mitchell had it by 1885, as and , knowing nothing of Frege, and it was their version, carried by Schröder to Peano and by Peano to Russell, that became the logic everyone learned. Peano turned the into in 1897 and Gentzen added to match in 1934. Hilbert and Ackermann’s textbook of 1928 was the first to set out the first-order logic of this chapter as a system on its own, and to ask the question about it that Part 2 answers. Then the letters came back as types: in Howard’s notes, in de Bruijn’s checker, in Martin-Löf’s foundation, and, over types rather than things, in Girard’s and Reynolds’s calculus, the one in your compiler. Church’s simple theory of types, the higher-order logic for which he typed the -calculus in 1940, is the ancestor of the provers that check mathematics in that logic today.
| Who | When | What |
|---|---|---|
| Frege | 1879 | function and argument; the quantifier, in two dimensions |
| Peirce, Mitchell | 1883–85 | the quantifier again, as and |
| Peano | 1897 | |
| Hilbert & Ackermann | 1928 | first-order logic as a system of its own |
| Gentzen | 1934 | ; the quantifier rules, with their side conditions |
| Church | 1940 | higher-order logic: the simple theory of types |
| Prawitz | 1965 | normalisation, quantifiers included |
| de Bruijn | 1968 | Automath: types that mention things, in a checker |
| Howard | 1969 | the quantifiers as a function from things and a pair |
| Girard; Reynolds | 1971; 1974 | over types: generics; second-order logic; normalisation |
| Girard | 1972 | proves everything |
| Martin-Löf | 1972– | and ; a foundation on dependent types, with universes |
| Mitchell & Plotkin | 1988 | modules as existential types |
| Wadler | 1989 | parametricity as theorems for free |
Summary
The quantifiers are and with the list of cases made unbounded: is an and with a case for every thing, written once with a parameter, and an or whose tags are things. Said as types, a proof of for all is a function from things, whose result type mentions its argument; a proof of there is is a pair of a thing and evidence. These are and , Peirce’s letters, which the logic traded for and and the type theorists kept. Rub the programs out and the four rules are Gentzen’s: let be arbitrary, in particular for , take to be , let be such a thing. The context enforces every freshness condition but the one on (E), which is why that one alone is written on its rule. The three ways of breaking the conditions are three bugs you’ve met, capture among them. Running a program still removes detours, every typed program still stops (proved by counting when there is no or , on loan when there is), the logic proves no atomic sentence, and a proof of there is names the thing.
Let the quantifier range over types instead of things and you get generics: one program for every type, written once and checked once. Its side conditions are the error messages rigid type variable and would escape its scope. The optional section finds the same programs from the logic’s side, by decorating second-order logic. There a schema becomes a single theorem, and , and are definable from and . Every program still stops (on loan, from Girard), so that logic is consistent, and a type such as says what its one program does.
A specification is a set of sentences, and “ follows from it” is a derivation, checkable by pattern matching: Hughes’s policy, a comparator’s contract, a sale. Since the logic proves no atomic sentence on its own, everything about Smith comes from the file. A type checker certifies that a program is safe to run. A verifier, a type checker whose types may say for all, certifies that it’s correct: what was asked of it is provable from what was assumed.
Three questions to ask of every proof system, then. Could it prove something false? We’ve shown that this one doesn’t prove everything, which is all that “false” can mean until the word has a meaning. Does it prove everything it should? We can’t even ask. Can the search for a proof be made mechanical, an algorithm rather than a genius? Not yet. And a puzzle to take into next week. Peirce’s law,
is a formula built from letters and arrows like any other. With type variables in place of the letters it is the type , in Haskell ((a -> b) -> a) -> a. Try to write a program of that type, in Haskell if you like, without recursion and without undefined. You won’t manage, and nothing so far can tell you whether that’s your fault or the formula’s. Part 2 begins by giving the formulas a meaning, and the first thing it finds out is what Peirce’s law is.
Exercises
Exercise 4.1. Write the typing tree for Smith’s ticket in full, from Hughes’s five declarations, and then the tree the checker builds for publish reyes ticket, marking the bar at which it fails and what the error message says. Then say what the context would have to contain about Reyes for the handler to type.
Exercise 4.2. Take and the extra fact .
Derive , as a tree, and give its program.
Try to derive with the (I) bar above the (I) bar, and say which condition stops you.
Add the policy line “nobody may read the audit log except administrators” to the signature and the policy, and say which of , and you needed, and why the obvious first attempt says something weaker.
Exercise 4.3. Derive and check that its program is
Then try the converse, , and say exactly which side condition stops you.
Exercise 4.4. Derive and , and their converses, giving the program for each. (These are distributive laws one level up: a function into a pair is a pair of functions, and a pair with a tagged second component is a tagged pair.)
Exercise 4.5. Write a comparator, in any language, that violates exactly one of the three lines of the contract, and say which consequence of the contract it breaks. Which violation would a sort routine detect, and how?
Exercise 4.6.
Derive from the empty context over a signature with a constant , and say why there is no derivation when the signature has none.
Derive , using (E), and say what program it is.
Try to derive the converse, , and say where you get stuck. What would the existence property have to give up for it to go through?
Exercise 4.7. Suppose (I) carried no side condition, so that a tree could generalise over a variable free in an open hypothesis.
From the hypothesis derive in one step, then , and then .
Conclude from the two hypotheses and , for constants and , which say only that one person is a maintainer and another isn’t.
Say which of the steps in (1) the real rule forbids, and state the corresponding compiler error.
Exercise 4.8. Write the types of length and map with their quantifiers explicit. Then type the use map show [1, 2], naming the instantiation at each quantifier. Finally, declare f :: a -> b and define f x = x: show the exact judgement at which the checker is asked to generalise over a variable that is not on top of the stack, and write the error in the words of the section on generics.
Exercise 4.9. Try to derive : “there is somebody such that, if they are a maintainer, everyone is”. Use the existence property to say what a closed derivation would have to hand over, and then say where every attempt sticks. Keep your attempt; Part 2 returns to this formula, together with from the exercises of the last chapter.
Exercise 4.10. (Optional section.) With and defined in second-order logic, type the programs , and of the proposition on the connectives, and run to normal form.
Exercise 4.11. (Optional section.) Show that the closed normal terms of type are exactly and . What are the closed normal terms of type , and what does that say about ?
Exercise 4.12. (Optional section.) Type and on , and give a program . Run to normal form.
Reading
Sørensen and Urzyczyn (2006, chapter 8) is the first-order half of the correspondence at the level of this chapter, with the rules for and written out and the normalisation theorem proved. Howard (1980), now for its second half, is where the quantifiers were first read as a function from things and a pair. Mitchell and Plotkin (1988) is the module side of abstract types; Wadler (2015) tells the whole story to programmers. Open Logic Project (2026, chapter 13) has the same rules at more leisure, and the Hilbert-style quantifier axioms this chapter did not need. de Bruijn (1970) is a founder’s account of a checker whose types mention things. Girard et al. (1989, chapters 11, 14 and 15) is the polymorphic -calculus by its inventor: the definitions of the connectives, the normalisation proof and the representation theorem. Wadler (1989) is parametricity, with examples a programmer can use.
References
- Alonzo Church. A formulation of the simple theory of types. Journal of Symbolic Logic, 5(2):56–68, 1940.
- N. G. de Bruijn. The mathematical language AUTOMATH, its usage, and some of its extensions. In Symposium on Automatic Demonstration, volume 125 of Lecture Notes in Mathematics, pages 29–61. Springer, Berlin, 1970.
- Gottlob Frege. Begriffsschrift, eine der arithmetischen nachgebildete Formelsprache des reinen Denkens. Louis Nebert, Halle, 1879. English translation by S. Bauer-Mengelberg in J. van Heijenoort (ed.), From Frege to Gödel, Harvard University Press, 1967, pp. 1–82.
- Gerhard Gentzen. Untersuchungen über das logische Schließen I, II. Mathematische Zeitschrift, 39:176–210, 405–431, 1934. Appeared 1934–35; English translation in The Collected Papers of Gerhard Gentzen, ed. M. E. Szabo, North-Holland, 1969.
- Jean-Yves Girard. Interprétation fonctionnelle et élimination des coupures de l’arithmétique d’ordre supérieur. Thèse de doctorat d’état, Université Paris VII, 1972.
- Jean-Yves Girard, Yves Lafont, and Paul Taylor. Proofs and Types, volume 7 of Cambridge Tracts in Theoretical Computer Science. Cambridge University Press, 1989. Free online.
- Norm Hardy. The confused deputy (or why capabilities might have been invented). ACM SIGOPS Operating Systems Review, 22(4):36–38, 1988.
- William A. Howard. The formulae-as-types notion of construction. In J. P. Seldin and J. R. Hindley, editors, To H. B. Curry: Essays on Combinatory Logic, Lambda Calculus and Formalism, pages 479–490. Academic Press, London, 1980. Written in 1969.
- John C. Mitchell and Gordon D. Plotkin. Abstract types have existential type. ACM Transactions on Programming Languages and Systems, 10(3):470–502, 1988.
- Open Logic Project. The Open Logic Text. https://openlogicproject.org, 2026. Release 9620cc7, 12 July 2026. Licensed CC BY 4.0.
- Charles S. Peirce. On the algebra of logic: A contribution to the philosophy of notation. American Journal of Mathematics, 7(2):180–202, 1885.
- Dag Prawitz. Natural Deduction: A Proof-Theoretical Study. Almqvist & Wiksell, Stockholm, 1965.
- John C. Reynolds. Towards a theory of type structure. In Programming Symposium, Paris, 1974, volume 19 of Lecture Notes in Computer Science, pages 408–425. Springer, 1974.
- Morten Heine Sørensen and Paweł Urzyczyn. Lectures on the Curry–Howard Isomorphism, volume 149 of Studies in Logic and the Foundations of Mathematics. Elsevier, Amsterdam, 2006.
- L. S. van Benthem Jutting. Checking Landau’s “Grundlagen” in the AUTOMATH System. PhD thesis, Eindhoven University of Technology, 1977.
- Philip Wadler. Theorems for free! In Proceedings of the Fourth International Conference on Functional Programming Languages and Computer Architecture (FPCA ’89), pages 347–359. ACM, 1989.
- Philip Wadler. Propositions as types. Communications of the ACM, 58(12):75–84, 2015.
Notes
The name is Norm Hardy’s, from a one-page paper of 1988 (Hardy, 1988). The deputy in his story was a compiler on a time-sharing system, which had permission to write into the system directory so that it could keep usage statistics there, and which also wrote a debugging listing to whatever file the user named. A user named a file in the system directory, the billing file, and the compiler, acting with its own authority on the user’s behalf, overwrote it. Hardy’s diagnosis was the one above: the compiler had been handed a name, when it should have been handed a credential that said what it was for. ↩
“I believe that the replacement of the concepts subject and predicate by argument and function, respectively, will stand the test of time” (Frege, 1879, preface), in the translation in van Heijenoort. It has. Frege’s own symbol for every was a dip in the horizontal line of a formula with a Gothic letter sitting in it. His formulas were two-dimensional, which is one reason almost nobody read the book. The notation lost; the analysis won. ↩
The code is Haskell with one change: an argument that the type mentions is named on the arrow, as in
(x : Person) -> Maintainer x, which is how languages whose types may mention values write such a type. Haskell doesn’t accept the five lines by default, because its types are not allowed to mention values. That is a design decision, not a limitation of the idea. ↩Two entries may be swapped when neither depends on the other, so the stack is a stack only as far as the dependencies go. A hypothesis that doesn’t mention can always be moved below it, and generalising over with such a hypothesis in force is fine, as it should be. ↩
is the type of an action that returns nothing of interest. ↩
de Bruijn (1970); the thesis is Jutting’s (1977). De Bruijn’s name sits beside Howard’s in the history of the correspondence, and this is why: he found the correspondence on his own, from the checker’s end, and built the checker. Howard’s notes of 1969 already had these two types, in their second half, as the constructions that go with the quantifiers of arithmetic. Martin-Löf made them the basis of a whole foundation for mathematics from 1972, after a first attempt that proved everything. ↩
Martin-Löf’s and may range over any type, including types that are themselves propositions, and in his system and are simply what and are called when the body is a proposition. The chapter’s and range over the signature’s one domain, which is the first-order case, and the one whose logic is Gentzen’s. ↩
Gentzen (1934); the Allzeichen first appears there, Peano’s dates from 1897. The side conditions, as Gentzen stated them for trees, are the ones in the definition below, and he called the variable they restrict the Eigenvariable, the variable proper to that step. ↩
All three are about theorems, with no hypotheses. From a hypothesis you can’t name a witness, since the hypothesis is a variable and a variable names nothing, which is why Baker’s log could say “declined or approved” without saying which. And with no constant in the signature, fails, since there is no term to be the witness: the logic assumes nothing it isn’t told, where most logic textbooks assume that something exists. ↩
Mitchell and Plotkin (1988) were the first to put it this way; the title of their paper is “Abstract types have existential type”. Girard found the polymorphic -calculus in 1971 and called it System F (Girard, 1972); Reynolds found it independently in 1974 (Reynolds, 1974). Its quantifier is the one behind generics in most typed languages. ↩
The method is reducibility candidates: a property of terms is defined for every type, and for the definition quantifies over all candidate properties, impredicatively, as the type does. The proof is in Girard et al. (1989, chapter 14). ↩
Girard’s representation theorem (Girard, 1972); Girard et al. (1989, chapter 15). Ackermann’s function, which no program built from bounded loops computes, is easy to write in System F. ↩
This is Girard’s paradox (Girard, 1972), a typed cousin of Russell’s. ↩