Chapter 3 ended one step short. Everything it proved was proved from no hypotheses at all, and nothing worth proving about a program is like that. Here are two things you would actually want to know.
The first is a bug report: “discount not applied on the last day of the sale.” A model proposes the fix below, a reviewer approves it, and the tests pass.
- return start <= today && today <= end;+ return start < today && today < end;
Is the patch correct? It makes the failing test pass. It also silently removes the discount on the first day of the sale, and no test covers that. It is perfectly well typed: no compiler will ever object to it, and by everything in Chapters 1–3 it is safe to run. Every yes you could give it rests on something nobody has checked — and what nobody has checked is what a sale is: that the discount applies on every day from the start to the end, those two included.
The second is what a sorting routine promises: that in the list it hands back, every element is at most the next one. Neither claim is a formula of Chapter 3. Both say for all, and that chapter had no way to say it; worse, it had no way even to name the things being quantified over — the days, the positions in a list — since its letters , , stood for whole claims and had nothing inside them.
So two things are missing, and the first is a language: one with terms that name things, predicates that say something about them, and quantifiers that range over them. That language is the subject of §4.1, and it is the language the rest of this course is written in.
The second missing thing is the rules, and here you are further ahead than you think. You
have been writing
for years without noticing. The type of length is [a] -> Int, which with the
quantifier it silently carries is
and your compiler has rules for that which it applies hundreds of times a second. They range over types rather than over days or list positions, so they are not the same quantifier — but they are the same rules, side conditions included, and §4.2 sets out the type theory they belong to before §4.3 writes them as logic. The chapter then ends where this part has been going: a sorting contract, proved.
4.1 First-order syntax
A type says what kind of thing a value is: this is an integer, that is a list of strings. It cannot say anything about which integer, and a specification is about which. To write “the discount applies on every day of the sale” we need symbols for days, for the start and the end, for the relation is on or after, and a way to say every. Here is the language that supplies them, and it is the one Frege arrived at in 1879 and nobody has had to replace.
Definition 4.1 (first-order formulas). A signature fixes some constant symbols , some function symbols and some predicate symbols , each function and predicate symbol with an arity, the number of arguments it takes; we always include the binary predicate . There is a stock of variables . The terms are the variables, the constant symbols, and for an -ary function symbol and terms . The formulas are defined inductively: is a formula for an -ary predicate symbol and terms (an atomic formula); is a formula; if and are formulas, so are , and ; if is a formula and a variable, and are formulas; and nothing else is. As before, abbreviates , and binary predicate and function symbols such as , and are written between their arguments.
A term names a thing; 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, and it would be a type error to write either where the other belongs. Here is the signature this chapter uses for its second half, which is an access-control policy — the rules that decide who may read what. The constants are users and resources, and ; the predicates are
A policy is then a set of formulas. “Administrators may read everything” is ; “owners may read what they own” is . Those are sentences a security team writes in English and a system enforces in code, and the question “can Alice read the config file?” is the question whether a particular formula follows from them. By the end of this chapter that question has a certificate.
Definition 4.2 (free, bound, substitution). In and the formula is the scope of the quantifier, which binds there, exactly as bound its parameter in Chapter 3. An occurrence of a variable is bound if it lies in the scope of a quantifier on that variable, and free otherwise; a sentence is a formula with no free variables. is the result of replacing every free occurrence of in by the term , bound occurrences left alone. A term is substitutable for in if no variable of lands in the scope of a quantifier on that variable when is put in for the free occurrences of : in words, arrives in with its variables still free. A closed term is always substitutable, and so is a variable on which has no quantifier. When the condition fails the remedy is the one Chapter 3 gave for : rename the bound variable to a letter that occurs nowhere else, and then substitute.
In the first two occurrences of are bound and the third free; the first is free and the second bound; so this is a formula but not a sentence. The term is not substitutable for in , since it would be captured; the term is. §4.3 shows what goes wrong when the condition is dropped, and that it is a bug you have already met.
4.2 Polymorphic type theory
Now the type system, built the way
Chapters 1 and 3 built theirs: types, terms, judgements, rules, and then some
typings written out. What is added to Chapter 3 is a quantifier over types, and
two rules for it.
First, why anyone wants one. Ask what type length should have.
It takes a list and returns a number, but a list of what? Of anything.
Without a way to say of anything you have three options, and all are bad.
- Write
lengthOfIntList,lengthOfStringList, and one more for every element type you ever use. That is the same code copied until one copy is fixed and the others are not. - Give
lengththe type[Object] -> Intand cast on the way in and out. This is what Java made people do until 2004, and the casts are unchecked: what used to be a compile error has become an exception in production. - Turn the checker off for that part of the program. This is where dynamic languages start and where the Mars Climate Orbiter ended.
One piece of code, one proof of safety, every element type at once, and nothing
unchecked anywhere: that is what the quantifier buys, and it is why map, filter,
sort, every container and every interface worth the name have it in their
types.
Definition 4.3 (polymorphic types and terms). Fix a stock of type variables alongside the basic types of Definition 1.9. The types are extended by two clauses: every type variable is a type, and if is a type and a type variable then is a type. In one line, with a basic type,
The terms of Definition 3.1 are extended by two clauses as well:
where is a type abstraction, the program made to work for every , and is a type application, that program used at the type . A context is now a finite list of two kinds of entry, type variables and typings , no name listed twice, and every type variable free in a typing in must be declared earlier in .
A type variable in the context is a type nobody has chosen yet. That is the only new idea here, and the two rules both turn on it.
| (4.1) |
Read (E) first, because it is the one you use every time you call a library function: a program that works for every may be used at any particular , and using it is substituting for throughout its type. Read (I) as what happens when a definition is finished: if you typed the body with in the context — that is, with standing for no type in particular — then the program works for every , and you may say so. Proved for an arbitrary , hence for all.
Example 4.4 (the identity, polymorphically). The identity function
id x = x should work at every type, and does. In the context
, rule (var)
gives ; (abs)
discharges ;
(I)
discharges .
- 1.
- (var)
- 2.
- (abs) 1
- 3.
- (I) 2
Line 3 is legal because by the time we reach it the context is empty, so is free in nothing. Using it at is one application of (E): . In Haskell you write neither the nor the — the compiler inserts both — which is why the quantifier is easy to use for years without seeing it.
Example 4.5 (twice, and where the type is forced). Let
. In the context
, two uses of
(app) give .
Two uses of (abs) then give
and (I) gives
Notice what the type rules out. There is no way to type unless returns what it takes, so the type could not have been : the second application would have nothing to feed on. The type is not a label we chose; it is what the two uses of (app) forced.
The side condition, which you have met as an error message. Rule (I) carries a proviso, and it is the only interesting one in this chapter. Generalising over is allowed only when is genuinely arbitrary, which means: fixed by no typing still in the context. Line 3 of Example 4.4 was legal because the context was empty by then. Inside the body it would not have been. With still in the context we may not conclude , because that would make a value of every type at once, and is one particular value whose type the caller will choose. Try it:
f :: a -> bf 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. The signature promised a program that works
for every
and every
separately; the body only works when they are the same. The compiler is enforcing the
proviso on (I),
and §4.3 will write the same proviso as a side condition on a rule of logic, where
it does the same job and catches the same mistake.
The other quantifier, and the code it is made of. A type can also hide something. 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 do not want to say what the state is, because that is the implementation’s business. In Haskell:
{-# LANGUAGE ExistentialQuantification #-}data Counter = forall a. MkCounter a (a -> a) (a -> Int)intCounter = MkCounter (0 :: Int) (+1) idlistCounter = MkCounter ([] :: [()]) (():) lengthtick3 :: Counter -> Inttick3 (MkCounter s step out) = out (step (step (step s)))
Both intCounter and listCounter are values of the one type Counter, although
one carries an Int and the other a list, and tick3 returns 3 for each without
being told which. The type of Counter is
there is a type with these three operations. Its two rules are building and
taking apart, and they are what MkCounter and the pattern match in tick3 do:
| (4.2) |
(I) is the constructor: supply the hidden type and a value of the interface at that type, and out comes a value that mentions neither. (E) is the pattern match: you may use the package to produce anything you could have produced from a fresh type variable and a value of the interface at .
The side condition on (E)
is the abstraction barrier, and it is worth seeing what it forbids. tick3 returns an Int, which
does not mention ,
so it type-checks. A function that tried to return the counter’s state would have result
type , and
is free in that
result type, so (E)
refuses. Try it and GHC says the rigid type variable a would escape its
scope — the same words, almost, as before, enforcing the same kind of
condition. The implementation type may be used inside the match and
may not leave it. That is what makes a module boundary a boundary,
and it is one side condition on one rule, not a convention anybody has
to remember. Mitchell and Plotkin (1988) were the first to put it this
way.
An honesty about the correspondence. The system just described is not an invention of these notes: it is the polymorphic -calculus, found twice, by Jean-Yves Girard in 1971 and by John Reynolds in 1974, and it is the core of every language with generics in it. Two things about it are worth keeping straight. Its and range over types, whereas the quantifiers of a specification range over the things a program handles — days, users, positions in a list — so the two are different quantifiers over different domains, and the logic whose proofs are polymorphic programs is a different and stronger one than the chapter builds. What is the same, exactly the same, is the four rules and their side conditions: a rule to instantiate, a rule to generalise that may not generalise a variable the context has fixed, a rule to package a witness, and a rule to unpack one that may not let the witness escape. Having seen them as type rules, you will recognise them in §4.3 on sight.
4.3 The quantifier rules
Chapter 3 gave each connective a rule that introduces it and a rule that eliminates it, and nothing else. The quantifiers get the same treatment, two rules each, and the four rules are the four moves of §4.2 written down.
Definition 4.6 (natural deduction, with quantifiers). Add to the rules of Definition 3.8 the following, in which is a term, a variable, and is substitution as in Definition 4.2.
| (4.3) |
A derivation is a finite list of judgements as in Definition 3.8, each justified by one of these rules or one of the ten there. I write , as before.
Read the four against the compiler.
- (E) is instantiation: a call. You have something that holds for every ; you use it at the in hand. The side condition is the one that stops being captured on the way in, and §4.3 is about what happens without it.
- (I) is generalisation: a definition. You proved of a about which you assumed nothing, so you have proved it of everything. Assumed nothing is the side condition, and it says in the logic exactly what “rigid type variable” says in the compiler: may not be free in any hypothesis still in use.
- (I) is building a module: supply the witness and the evidence , and what comes out mentions neither. A proof of is the pair.
- (E) is using one: from you may go on to anything you could have derived from for a fresh . The three conditions on are the three ways of cheating, and together they say: you may use the thing, but you may not learn which thing it is. That is a module boundary, written as a side condition.
(I) says everything equals itself and (E) is Leibniz’s law: equals may be substituted for equals in any claim. Symmetry and transitivity of are not rules because they follow from these two (Exercise 4.1).
Notice what is not here, and was in Chapter 2: a Deduction Theorem. There is nothing left for it to say. Its content — that anything provable on the supposition yields without the supposition — is the rule (I), which this system simply has. Where Chapter 2 had to prove that supposing was safe, and pay for every supposition with instances of (II), natural deduction supposes and discharges as a matter of course. That is what makes the proofs below short enough to read.
Where the eigenvariable condition bites. The proviso on (I) is worth one example of its own, because getting it wrong is a real class of bug and not a technicality. Suppose the policy is being checked for a particular user, and the hypothesis in use is : this user is an administrator. One application of (I) would give : everyone is an administrator, from which, with the policy, everyone may read everything. The rule forbids it, and the condition is exactly the right one: is free in the hypothesis , which is still in use, so is not arbitrary. Discharge the hypothesis first, by (I), and generalisation becomes legal — but then what you have generalised is , which is the harmless and true thing. Exercise 4.5 runs the bug to .
Substitutability: the same bug twice. Rule (E) lets you go from to — what holds of everything holds of — if is substitutable for in . Drop the condition and the system proves from a specification any programmer would write. Let the one hypothesis be
“no element is the only element”, which is what you say of any collection with two things in it. Take to be and to be the variable , which is not substitutable for in , since it would land under the ; and pretend (E) did not care.
- 1.
- hypothesis
- 2.
- the illegal (E) on 1,
- 3.
- (I)
- 4.
- (I) on 3; is free in no hypothesis
- 5.
- (E) 2, 4, since line 2 is
Five lines, and the system has proved from a harmless hypothesis: exactly the disaster Theorem 3.19 ruled out, and the only thing that let it happen was line 2. What went wrong is visible in the formulas. The we substituted meant “some given element”; on landing under it came to mean “whichever the quantifier picks”. With the condition in force, line 2 is not 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.
You have met this bug, and not only in logic. It is variable capture, the bug that clause 5 of substitution in Chapter 3 exists to prevent. Running a program was substitution, , and it needs the same condition. Take — , the function that keeps its first argument — applied 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 is given, and 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 ; 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; and a language implementation never substitutes text — it carries the argument’s value in an environment — which is why you have met the bug in macros and refactoring tools, which do, and not in function calls.
4.4 Derivations as trees
Definition 3.12 built derivations as trees for the connectives, and tracked for each one the set of hypotheses it still rests on. Four clauses extend it to the quantifiers, and the side conditions become easier to state when they are written this way, not harder — which is the reason for doing it.
Definition 4.7 (derivations, with quantifiers). Add to the clauses of Definition 3.12 the following four.
- 10.
- (E) If
concludes
and
is a term
substitutable for
in ,
then
- 11.
- (I) If
concludes
and the variable
is free in no
formula of ,
then
- 12.
- (I) If
concludes
and
is substitutable
for
in ,
then
- 13.
- (E) If
concludes
and
concludes
, and the
variable is free
neither in , nor
in , nor in any
formula of ,
then
is a derivation, with .
The variable in clauses 11 and 13 is called the eigenvariable of the step, and it is the only thing in this chapter that needs care. Read the two conditions. What the condition says, and why the tree says it better. Clause 11 asks that be free in no open hypothesis of the subderivation. That is the exact sense of “ was arbitrary”: nothing the derivation still leans on says anything about in particular, so whatever was shown of was shown of anything. Compare the list form, rule (I) of Definition 4.6, which asks that be free in no formula of . That is sound, but it is coarser: is whatever set the line happens to carry, and may include formulas the derivation never used. A tree carries exactly the hypotheses it needs, so the condition it states is exactly the condition that matters. This is the one place where the tree notation is not merely nicer to read but says something sharper, and it is why proof theory is written in trees.
Clause 13 has three conditions and each rules out one way of cheating.
- not free in : you may not let the witness escape in the answer. “Some user can read the file; call them ; therefore can read the file” is not a valid step, because was a name you made up.
- not free in : the fresh name must really be fresh, and not a variable the existential claim already mentions.
- not free in the other open hypotheses: nothing else in use may already say something about .
Those three are the type-theoretic condition of (4.2) — the implementation type may be used inside the match and may not leave it — translated word for word, with for the type variable . Compare them against the rule (E) you have already read on page 138: a derivation that breaks one of them is a program that GHC rejects with rigid type variable would escape its scope.
The policy, as trees. Here is the access-control policy of §4.1 at work. Write
— administrators may read anything, and owners are administrators — and suppose we are also told . Can Alice read the config file? Two instantiations and one application:
Three bars; the open hypotheses are the one policy line and the one fact, so the tree witnesses . It is a certificate: an auditor who disbelieves the answer need not rerun the system, and need not understand it either. They check three bars.
Now something no policy engine can settle by evaluation, because it quantifies over all users and all resources at once: from , every owner may read what they own. Let be the derivation that puts the two policy lines together on a supposition — four instantiations and two applications, the moves just used — ending in .
The supposition sits at a leaf of and is discharged at the first bar. Only then are the two generalisations legal, and now you can check that against clause 11 rather than take it on trust: after the discharge, of the subderivation is just the two policy lines, and both are sentences, so neither nor is free in any of them. Had we generalised before discharging, would still have been open, would have been free in it, and the clause would not have applied — which is the formal version of the complaint that we were claiming the policy for all users on the strength of one.
A proof of there is names the thing. One more, and it is the one that pays. From the same policy and , one more bar on the first tree gives
somebody can read the config file. Read as a program, the step packs the witness together with the evidence that she qualifies. And that packing is not a convenience of the notation: in this logic it is the only way an can be introduced, which has a consequence worth stating.
Remark 4.8 (a proof of there is hands you a witness; on loan). If , then there is a term with . A proof that something satisfies does not merely rule out that nothing does: it produces a thing that does.
Why it is believable: a closed proof of is, read as a program, a closed program of that type; run it, and Lemma 3.18 says that what it runs to must be an introduction form; and the only way to introduce an is (I), which packages a term with a proof of . The witness is the first half of that package — the very you supply when you build a module in §4.2. We take it on loan, as we took Theorem 3.16, because the step the argument needs is a normalisation theorem for the quantifier rules, which we have not proved.
That property is exactly what an audit wants. “Is this file readable by anyone outside the team?” is an existence question, and an answer of yes that cannot say who is no use to a security engineer. In this logic every yes comes with the name attached.
It is also strong enough to be worth distrusting. “Either the program halts or it does not” is the kind of thing one would like to assert without producing either a halting run or a proof that there is none; “some input breaks this function” is the kind of thing one would like to assert without producing the input. In the logic of these four chapters you may assert neither. Whether that is a virtue or a restriction is a question we cannot yet ask, because asking it needs the word Part 2 supplies.
4.5 Sorting, and what its type cannot say
Here is insertion sort, written so that the comparison is a parameter — which is how every sorting routine in every standard library is written, because the library cannot know what you are sorting.
sort(lt, xs):out = []for x in xs:i = 0while i < length(out) and lt(out[i], x):i = i + 1insert x into out at position ireturn out
Everything in this part applies to that program. It has a type, and the type is polymorphic in the sense of §4.2:
one program for every element type, instantiated by (E) at each call. The checker proves that type, and by Chapter 3 the proof is the program. So the compiler has certified something real: this code will not add a string to a number, will not call a non-function, and will hand back a list of the type it was given.
It has certified nothing whatever about sorting.
The gap is the parameter lt. Give sort a comparison that returns True
whenever the two arguments are equal — <= where < was meant, the single
commonest mistake in the business — and the type is unchanged, because <= and
< have the same type. The program compiles, runs, and returns a list;
the list is sometimes not sorted; and on a longer input the routine may
do something stranger. This is the exception Java’s sort really throws:
java.lang.IllegalArgumentException:
Comparison method violates its general contract!
Read it carefully, because it is unusual and exact. The sort has not checked the contract — checking it would need every pair of elements, which costs more than the sort. What it has noticed is that its own bookkeeping has become impossible: it merged two runs it had recorded as ordered and found them not ordered, which cannot 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 neither is possible in a type.
The contract. What sort needs of lt is that it be a strict
total order: three sentences, in the language of §4.1, with
for
lt(x,y).
What the library needs | As a sentence |
any two elements compare one way, the other, or are equal | |
nothing is less than itself | |
if and then |
Call those three .
They say for all, so no type can hold them; they are about the
elements, so Chapter 3 could not write them; and they are what the
library’s documentation is trying to say in English. Now the consequence.
What a comparator built from <= violates is asymmetry: it reports
and
for a
pair of equal elements, and no comparator obeying the contract can.
That is not 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. Write for together with the supposition .
- 1.
- (hyp), the supposition
- 2.
- (hyp), transitivity
- 3.
- (E) on 2,
- 4.
- (E) on 3,
- 5.
- (E) on 4,
- 6.
- (E) 5, 1
- 7.
- (hyp), irreflexivity
- 8.
- (E) on 7,
- 9.
- (E) 8, 6
- 10.
- (I) 9, discharging the supposition
- 11.
- (I) on 10
- 12.
- (I) on 11
Read it in four blocks. Lines 2–5 strip the three quantifiers off transitivity, choosing at the last step; that is the step that says “then ”. Line 6 fires it on the supposition. Lines 7–9 strip irreflexivity and reach . Line 10 discharges, which is what turns “the supposition leads to absurdity” into “the supposition is false”, and lines 11–12 generalise. Both generalisations satisfy the side condition, and now you can say exactly why: after line 10 the hypotheses in use are the three contract sentences, and a sentence has no free variables, so neither nor is free in any of them.
Two things about the shape of that derivation are worth having. The supposition did all the work: there is no axiom in it anywhere and no theorem imported from elsewhere, because (I) lets the derivation suppose and then pay for the supposition. Chapter 2 could not do that. In its system the same fact needed contraposition proved separately and brought in as a line, and it needed the Deduction Theorem to license the supposition at all; here supposing is a rule, and that is the whole difference the move to natural deduction bought. The second thing is that every line is checkable by pattern matching against (4.3), side conditions included, so the twelve lines are a certificate in the sense this part has used throughout.
So: a comparator obeying the contract can never report both and . When the sort catches a comparator doing exactly that, the exception is reporting a theorem being violated — a theorem the type checker could not state, let alone prove, and that a verifier, which is a type checker whose types may quantify, could. That is the whole distance between safe and correct, measured on a program you have called a hundred times.
4.6 Checking is easy; finding is not
We can now write down the question “does follow from the specification ?”, and when the answer is yes a derivation exists. It is tempting to conclude that we have an algorithm for the question: to decide it, “just” check whether a derivation exists.
Problem 4.9. Why is that conclusion wrong?
Because you do not know how long a derivation of , if there is one, might be. Suppose you knew a bound: that whenever follows from at all, it does so by a derivation of at most a thousand symbols. Then you could enumerate the finitely many strings of at most that length and check each, and the rules of (4.3) would do the rest. (A bound on the number of lines would not do: each line can be as long as you like.) But no such bound is known, and a search that has not found a derivation yet cannot tell whether to give up. What we have is a checker for certificates and no finder. This is the point at which the ambition of the next part declares itself. If a proof can be checked mechanically, why can it not be found mechanically? Mathematics would then be a thing one could automate: state the specification, state the claim, and let the machine report. That programme is not a fantasy and it is not finished, and Part 2 is largely about how far it goes. The short answer is that it goes further than you would guess and stops sooner than Hilbert hoped.
Your compiler, meanwhile, is in exactly the position we are in: it checks the proof your program is; it does not find one. You have, by now, tools that will hand you a program of a type you name, or a patch for a bug you describe, and what they hand you is a candidate. The compiler’s check is what makes a candidate more than a guess, and when the type can say for all, the check is a verification against the specification. Finding has become cheap; checking is what is left to trust, and it is the subject of this course.
A specification is a set of sentences — three lines of a comparator contract, two lines of an access-control policy, whatever a sale is — and “ follows from the specification” is a precise, checkable claim. The language that states it is first-order logic (Definition 4.1); the rules that certify it are Gentzen’s, four of them for the quantifiers (Definition 4.6), and they are the compiler’s two moves for a polymorphic type and its two ways with a module, side conditions and all. A proof of an hands back the witness it was built with (Remark 4.8). 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 is correct, meaning: the postcondition is provable from the precondition and the specification. The discount patch is correct or not according to three or four sentences about sales, and once they are written down the question has an answer a checker can verify.
Checking is easy. Finding is not. Proof checking is pattern matching, and it is what your compiler does; proof finding, so far, is cleverness. Three questions about any proof system. Could it prove something false? — we have shown that this one does not prove everything (Theorem 3.19), which is all that “false” can mean until the word has a meaning. Does it prove everything it should? — we cannot even ask. Can the search for a proof be made mechanical, an algorithm and not 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. Try to write a program of that type before next week. You will not manage, and nothing in this part can tell you whether that is 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. Derive symmetry, , and transitivity, , from (I) and (E) alone. (For symmetry, suppose and apply (E) to the instance of (I), substituting in the left-hand slot.) Why are symmetry and transitivity therefore not rules?
Exercise 4.2. Take of §4.3 and the extra fact .
- (1)
- Derive , as a list and as a tree.
- (2)
- Write out in full the derivation that the second tree of that section left as a name, and then the whole tree, checking the side condition at each (I).
- (3)
- Add the policy line “nobody may read the audit log except administrators” and say which of , , you needed and why the obvious first attempt says something weaker.
Exercise 4.3. 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.4.
- (1)
- Derive .
- (2)
- Derive , using (E), and say what program it is.
- (3)
- Try to derive the converse, , and say where you get stuck. What would Remark 4.8 have to give up for it to go through?
Exercise 4.5. Suppose (I) carried no side condition.
- (1)
- From the hypothesis , for a constant , derive in one step, then , and then .
- (2)
- Conclude from the extra hypothesis , which says only that is not the only thing there is.
- (3)
- Say which of the two steps in (1) the real rule forbids, and state the corresponding compiler error.
Exercise 4.6. 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 line of the typing at which the checker is asked to generalise over a
variable the context has fixed, and write the error in the words of §4.2.
Reading
Girard et al. (1989, chapters 2–4) again, for natural deduction with quantifiers and the normalisation theorem Remark 4.8 borrows; Open Logic Project (2026, chapter 13) for the same rules at more leisure, and for the Hilbert-style presentation this chapter did not need. Mitchell and Plotkin (1988) for modules as existentials. Next, Part 2: what the formulas mean, and what that reveals about the logic this part has been using. References.
- 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.
- 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.