Your compiler has refused to run your code before. Not a crash and not a warning: a red line under something that would have gone wrong had the code run. The function was never called. No input was ever tried. And yet the compiler was certain — certain enough to refuse — that it could not vouch for the code. Certainty that has cost nothing is either a swindle or a theorem, and this part of the notes is an attempt to find out which.
First, why there is a checker there at all. Inside the machine everything is bits, and a pattern of thirty-two of them is an integer, or four letters of text, or a colour, or the address of something else, according to nothing but what the program means to do with it. The processor does not know and does not care: asked to add a colour to an address, it will. So the oldest and commonest kind of bug is a thing of one kind put where a thing of another kind was expected. In 1999 the Mars Climate Orbiter was lost because one program reported an impulse in pound-force seconds and the program that read the figure took it for newton-seconds (Mars Climate Orbiter Mishap Investigation Board, 1999): a number of one kind, consumed as a number of another, and a spacecraft flew into the atmosphere of Mars. No test caught it. A checker that knew the kind of each number would have refused to compile the hand-over.
That is why every language you use has a type checker. Java and Rust were born with theirs; JavaScript grew one, called TypeScript, when the untyped web became unmanageable; Python acquired the means for one in 2015. The checker is the part of the compiler whose job is to say no, before anything runs, to every program that would put a thing where a different kind of thing belongs.
Now look at what it claims when it says yes. It claims that the function is safe to run on every input it could ever receive: that it will never add a string to a number, never call something that is not a function, never hand back a value of the wrong shape. A test checks the inputs you thought of. The checker has run none and vouches for all of them. Whatever it is doing, it is not testing. So: what is it looking at, and what is a type?
1.1 Terms and reduction
To understand what the compiler did we must first see what it saw, and the difficulty is that there are hundreds of languages, each with its own checker, and no two of them look alike on the page. The way through is the way a mathematician always takes when the particulars are many and the pattern is one: abstract. Keep only what every checker attends to, and strip away the rest. What does a type checker attend to? Not what your function computes; only what it takes and what it returns. Not the value of an expression; only how it is put together. Not the keywords, the numbers, the names of the library functions; only which thing is applied to which. Strip all of that away and very little is left — less than you would think. There are things that are applied, there are things they are applied to, and there is the act of applying one to the other. Here is a language with exactly that in it and nothing else.
Definition 1.1 (terms, definitions, programs). Fix two constants, and , and a stock of variables . The terms are defined inductively:
- 1.
- Each constant, and , is a term.
- 2.
- Each variable is a term.
- 3.
- If and are terms, then is a term: the application of to .
- 4.
- Nothing else is a term.
A definition is an equation
where is a new name, are distinct variables, and is a term built from constants, names defined earlier, and the variables alone. Once defined, the name may be used as a term.
The symbol is read “is defined to be”, and it is not the of arithmetic: it introduces a name rather than claiming that two things are equal. The last clause of the term definition is what “inductively” means: a term is whatever you can reach from constants and variables by finitely many applications, and nothing you cannot. In one line, with and standing for terms already built,
Application groups to the left:
is ,
a function of two arguments taking them one at a time, and
brackets are written only where the grouping is to the right, as in
. In
Haskell the term language is data Term = K | S | Var String | App Term
Term, one constructor per clause.
A term is closed if no variable occurs in it, and a closed term is what I shall call a program. The word is meant literally: in a closed term there is nothing left to supply. A term with variables in it, such as the body of a definition, is a fragment — it says what to do once , and are given, and until they are given there is nothing to run. The distinction earns its keep in §1.3, where the checker has to be told the kinds of the variables a fragment leaves open, and it is why the checker’s judgements carry a list of them.
Example 1.2 (definitions you have already written). Three definitions in the sense of Definition 1.1:
In Haskell they are twice f x
= f (f x), comp f g x = f (g x) and flip f x y = f y x; in Python,
three defs. You have written all three. Read the first against the clauses:
is a new name,
and
are distinct variables, and the body
is built from those two variables by application alone, which clause 3 allows.
What a definition may not do is reach outside itself. The right-hand side
may use the two constants, the parameters, and names defined earlier — and
nothing else: no numbers, no strings, no if, nothing from a library. That
restriction looks crippling, and Examples 1.5 and 1.6 are here to show that
it is not.
A definition is a rule for rewriting: wherever stands next to arguments, the left-hand side may be replaced by the right, with the arguments written in for the parameters. The two constants are the two definitions we take as given, and this is what they do.
Definition 1.3 (running). A step rewrites a term in one of three ways:
the third whenever is a definition. The part rewritten may sit anywhere inside a larger term. A term in which no step can be taken is in normal form. To run a term is to take steps until a normal form is reached; I write when can be reached from by some number of steps, zero included.
So is the function that keeps its first argument and drops its second, and is the function that hands its third argument to both of the others and applies the results. Terms built from constants by application, together with rules for rewriting them, make a combinatory algebra; the constants are called combinators, and these two are Schönfinkel’s. Neither looks like anything you would want; the point of the next few pages is that between them they are everything you have ever wanted. One convenience: we shall allow ourselves the integers and the operator as ready-made constants, with the obvious step the sum. They can be built from and alone, and Example 1.6 shows how, but nothing here turns on it.
Example 1.4 (the identity, and composition). The function that returns its argument unchanged is not one of the two constants, but it is in the language. Define
a definition with no parameters, and run it on an argument:
Whatever is, runs to . The of Example 1.2 can be written from the constants alone too: Exercise 1.2 asks you to check that does the same job. I write for that program.
Example 1.5 (booleans). A boolean is a choice between two things, so let be the function that keeps the first of two arguments and the one that keeps the second:
With that choice, if b then M else N is simply
: the
boolean applied to the two branches. Run both cases.
There is no if in the language and none is needed: the boolean is the
conditional.
Example 1.6 (numerals, and addition). A number is “do it times”: a function that takes an and an and applies to that many times. One definition for each ,
To add one to a number, apply once more; to add two numbers, apply the first number of times to the result of applying it the second number of times:
Here is at work. To see a number come out we need an that does something visible, so let and run on and , one step per line, with the definition unfolded shown on the right:
Nine steps, and the answer is : applied to and the program computes exactly what computes. Two remarks. First, the numerals are definitions with parameters, so by the promise of Theorem 1.8 below each is a closed term in and , and the ready-made integers were a convenience and not a need. Second, has a one-line form, : check that .
Two things have been taken on trust in these examples, and it is worth saying which. At the fourth line of the last run there were two steps available, since could have been unfolded at the outside instead of the inside; I chose the inside. The choice does not matter.
Remark 1.7 (the order of steps does not matter; on loan). If a term runs to a normal form, it runs to only one: two runs of the same term that both reach normal form reach the same one. This is a theorem of Church and Rosser (1936), and we take it on trust. It is what lets us speak of the result of a program.
The second is larger, and it is the reason this calculus and not some other is the one to study.
Theorem 1.8 (two constants suffice; on loan). Every definition by equations can be replaced by a program in and alone that runs the same way on every argument; and the functions on numerals that such programs compute are exactly the functions a Turing machine computes.
The first half is Moses Schönfinkel’s (1924): he noticed that the variables in a definition are bookkeeping, and that two fixed combinations do all the bookkeeping there is. The second half is Alonzo Church’s (1936) and Alan Turing’s (1937), who proved it in Princeton in the year he arrived there to write his doctorate under Church. Between them the two halves say that this small language is not a toy: anything any computer can compute, a program in and can compute, and anything you can write with parameters you can write without them. We shall not prove the second half in this course. The first half we shall prove in outline, and it is the surprise of Chapter 2: compiling a definition’s variables away turns out to be a theorem of logic (Theorem 2.7).
Take stock. Nothing has been added to your idea of a program; a great deal has been taken away, and what is left is this. A program is a finite list of definitions together with a term to evaluate. A definition is an equation. To run the term is to replace, over and over, a left-hand side by its right-hand side, until no replacement is left to make. There is no store, no stack, no instruction pointer, and no order of evaluation to settle: which step you take first cannot change the answer you get (Remark 1.7), though Exercise 1.4 shows that it can change whether you get one at all. The constructs you might think are missing have been built in front of you out of nothing — the conditional in Example 1.5, the natural numbers and addition on them in Example 1.6 — recursion is Exercise 1.6, and Theorem 1.8 says that whatever else you want is there too.
So this is not a small language chosen because it is convenient to reason about. It is the general case, reached by deleting from your own languages everything a checker does not read. That is worth holding on to for the next two pages, because something is about to go badly wrong in it, and it will not be a quirk of a toy. It will be a thing that goes wrong in yours.
1.2 Non-termination
Nothing in Definition 1.1 says what may be applied to what. A boolean is a function and a number is a function, so of course a function may be applied to either; and a function may be applied to itself. The definition
is legal: its right-hand side uses nothing but its one parameter. So is the function that applies its argument to itself — an odd thing to want, perhaps, but not a difficult thing to write. Apply it to itself,
and run it. There is one step available, the definition of with written in for , and it gives
The step returns the term it started from. So there is a second step, and it returns it again, and no number of steps ever finishes.
Be exact about what that costs, because “does not terminate” is a phrase the eye slides over. Run and there is no output, no error and no crash. The machine is working. It will still be working tomorrow. You have met this: a page that spins; a build that never finishes; a request whose reply never comes, holding its connection open, and then a thousand more behind it, until a server that was answering everybody is answering nobody. The failure here is not that a program returned the wrong answer — a wrong answer can at least be read, and argued with. It is that the program returned nothing and took the machine with it.
Now look at the size of the thing. is four symbols. It is not the end of a long chain of subtle mistakes; it is two sensible decisions in a row. Nothing in the three clauses of Definition 1.1 objected, and nothing in them could: they say which strings are terms, and is a perfectly good string. The one rule of Definition 1.3 did not object either. It had a step available, and it took it. (Compiled into the constants, is — check that — and is , which takes three steps to come back to itself instead of one. Nothing is gained by compiling; the loop is in the shape, not the notation.)
Nor can you expect to see it coming. announces itself because it is four symbols long. The same self-application, arrived at through three library calls and a type nobody wrote down, announces nothing at all, and the paragraph above is the whole of the warning you are going to get. It is a fact, though not one we are yet in any position to prove, that no checker whatever catches every program that fails to stop. So we shall ask for something weaker, and get something more useful: a checker that refuses a class of programs we can describe exactly, and that has inside that class.
1.3 Types and typings
What went wrong is not hard to name. The function takes an argument and returns a result, and nothing anywhere recorded what kind of argument it takes or what kind of result it gives back. So nothing was in a position to notice that when was handed , it had been handed something it was never built to take. The language has functions and arguments and no bookkeeping whatever. So here is the idea, and the rest of this chapter is its execution. Write the bookkeeping down. Label every term with what it takes and what it returns; allow an application only when the argument’s label is the one the function asks for; and check, before anything runs, that the labels fit together throughout. Read a label as a contract: give me an and I shall return a . The virtue of a contract is that it can be checked without being honoured — without running anything — because it speaks of kinds and not of values, and a verdict in advance is exactly what we are asking for.
Three things must be settled before that is a checker rather than a hope: what the labels are, what it means for a term to carry one, and which labellings are allowed. The labels are called types, and they are built by two clauses, as a term was built by three.
Definition 1.9 (types). Fix a stock of basic types, such as , and ; I write for basic types when it does not matter which. The types are defined inductively:
- 1.
- Every basic type is a type.
- 2.
- If and are types, then is a type: the function type, the type of functions that take an and return a .
- 3.
- Nothing else is a type.
In one line, . So is the type of a test on integers, and is a type too; look at the shape of that one for a moment before reading on. The arrow groups to the right, so is , a function of two arguments taking them one at a time, just as application grouped to the left. Every real checker has more — records, lists, generics — and every one of them has this.
To type a term with variables in it you must know their types.
Definition 1.10 (context, judgement). A context is a finite list
pairing each variable in scope with a type, no variable listed twice; the empty list counts, and I write for with the pair added at the end. The checker’s basic claim is a judgement
read “ has type when its variables have the types gives them”. When is empty I write simply , and then is a program with type outright.
A checker works down a term a piece at a time, and by Definition 1.1 there are four kinds of piece. The two constants have fixed types, one each. A variable has the type the context gives it. An application is typed from the types of the two terms in it, and it is there that a checker says no. Four cases, then, and the certificate the checker produces is a list of judgements of which every line is one of the four.
Definition 1.11 (typing). The two constants have the types
where , and stand for arbitrary types: each line is a schema, and gives one type for each choice of letters. A typing of is a finite sequence of judgements
all carrying the same context , whose last line is , and in which each line is justified in one of four ways:
- 1.
- (K) is , and is an instance of (1.1);
- 2.
- (S) is , and is an instance of (1.2);
- 3.
- (var) is a variable , and is in ;
- 4.
- (app) there are two earlier lines and , with , such that is the type and is the term .
No other kind of line is allowed. On the right of each line I write its justification: “(app) , ” names line as the function and line as the argument, and after (K) or (S) I give in square brackets the types put for the schema’s letters.
Read the four cases as the checker reads your code.
- (K)
-
takes an , then a , and returns the , and (1.1) says so for every choice of and .
- (S)
-
takes a function of two arguments, a function of one, and an argument , and returns . For that to make sense must be what both and expect:
and the result is then a . Schema (1.2) is that sentence written down.
- (var)
-
A variable has the type it was declared with, and the checker has only to look up in .
- (app)
-
An application is well typed only if is a function and is an argument of exactly the type expects, and then the result has ’s result type. When the types do not match, this is the case that draws the red line.
The ready-made pieces get cases of the same kind: every integer has type , the operator has type (so that is applied to and then to ), and a string such as has type .
What the definition has no case for is a name. It types constants, variables and applications, and a name introduced by a definition is none of the three: to type the checker would need the type of , and nothing above gives it one.
Example 1.12 (the identity, typed). Here is the typing of at the type . The square brackets name the types substituted for the schema’s letters, in order.
- 1.
- (S)
- 2.
- (K)
- 3.
- (app) 1, 2
- 4.
- (K)
- 5.
- (app) 3, 4
Five lines; the context is empty throughout, since there are no variables. Line 1 is the line nobody would write unprompted, and it is found the way such lines always are, backwards from the goal: we want , so ’s must be and its must be , which leaves its free; and lines 2 and 4 both go through if is chosen to be . The search took a sentence; the check takes none.
Example 1.13 (the red line). Now try to type .
- 1.
- ready-made
- 2.
- ready-made
- 3.
- (app) 1, 2
- 4.
- ready-made
Line 5 should be (app) applied to lines 3 and 4. But (app) needs the argument’s type to be the of the function’s , and here the function wants and is offered . Nor is there another way round: since accepts nothing but , line 3 gives the only type the function can have. No rule applies; there is no typing; the checker refuses. That refusal — expected Int, got String — is the content of the error your editor would show, stripped of the decoration a real language adds.
The refusal that matters most is the refusal of , and to see it we need one fact about : not just that it has type , but that it has no type of any other shape. The argument reads a typing backwards from its last line — the last rule used must have been such-and-such, so the lines above must have looked so — and that shape of argument is the shape of every proof in Chapter 2.
Lemma 1.14. If then is for some type .
Proof. Read the typing backwards. Only case (app) puts an application on a line, so the last line came from two earlier ones, and the first of those from two earlier again:
Now put the two schemas against those four lines.
- 1.
- Only case (S) types the constant , so has the shape : that is, , and .
- 2.
- Only case (K) types the constant , and it gives types of the shape . The type has that shape, which forces .
- 3.
- The type has that shape too, which forces to be some .
Hence . □
Proposition 1.15 ( has no type). There is no context and no type with . Consequently has no typing.
Proof. Suppose there were, and read the typing backwards as in the lemma. The last line came by (app) from and , and the first of those from and . By (S), and for some , , . By Lemma 1.14, both and have the shape : so from the first and from the second, and therefore
No type satisfies that equation. The type contains every symbol of and an arrow besides, so it is strictly longer than , and equal types have equal length. So has no type; and a typing of would contain a typing of its left half, so has none either. □
The work was done in two places. The lemma and the first half of the proof only read the four cases backwards; the argument then rests on one fact about types that is not one of the cases, that a type cannot be a proper part of itself. The same fact, seen directly: in a context where , the body of would need and at once, and (var) gives the one type , so . The parameter was the problem, and compiling it away did not make it go away.
So the checker refuses , and refuses it by a failure to match, like the red line. That is a refusal of one program. Two theorems say what the checker’s yes is worth for all the others, and the first of them we can prove.
Theorem 1.16 (subject reduction). If and by a step, an step or a ready-made step, then .
Proof. First the case in which the whole of is the part rewritten, and then the case in which it sits inside something larger.
A step. Then is and is . Read the typing of backwards twice. The line came by (app) from and , and the line came by (app) from
Schema (1.1) gives no types but those of the shape , so . Hence , which is what we want.
An step. Then is and is . Reading backwards three times gives
and schema (1.2) forces , , and . Now build the typing of forwards:
- 1.
- (app) on and gives ;
- 2.
- (app) on and gives ;
- 3.
- (app) on those two gives , and is .
A ready-made step. the sum: both sides have type .
Inside a larger term. If the part rewritten is a proper subterm of , the lines of the typing that type that subterm are replaced by the lines just constructed, which end in the same type; every later line of the typing is unchanged, because (app) looks only at the types of its two parts and not at what they are. □
Read what that says. The checker gave its verdict before the program ran. Subject reduction says that the verdict is not overtaken by events: at every step of the run, the term in hand still has the type the checker assigned, so an argument of the wrong kind can never arise part-way through, however long the run. What the checker said stays said. That is the first half of the certificate’s worth. The second half is about , and about every program like it.
Theorem 1.17 (normalisation; on loan). Every program with a typing runs to a normal form.
This we take on trust; the proof (Tait, 1967) uses a technique this course does
not reach. We have seen the one looping program we know of refused, and
Proposition 1.15 says why; the theorem says that no typed program loops, which
is a great deal more, and we shall lean on it twice in Chapter 3. Every real
language gives the loop back, by adding recursion under another name —
Haskell’s fix, a while, a method that calls itself — and Exercise 1.6 asks what
that costs.
What Church built it for. Church did not offer this calculus as a programming language. In 1932 there were no programs to write, and what he offered it as was a logic (Church, 1932): a way of writing statements, and of writing the functions that take statements to statements, in exactly the terms of Definition 1.1. The plan was that the whole of mathematics might be done in it. Within three years it was broken, and what broke it was the self-application of §1.2 (Curry, 1942; Kleene and Rosser, 1935). Here is the shape of what went wrong:
Because anything may be applied to anything, a statement can be made to talk about itself. Fix any statement you please — let it be “”, if you want something plainly false — and let be the statement
“if holds, then holds.”
That can be written down at all is the self-application of §1.2 over again: names the very statement it sits inside, as was applied to . Now reason about it, in five steps.
- 1.
- Suppose .
- 2.
- says that implies , so on that supposition we have .
- 3.
- From (the supposition) and (step 2) we get .
- 4.
- Steps 1–3 got out of the supposition . Discharge it, and conclude — now with nothing supposed.
- 5.
- But is exactly what says. So we have outright; and then, by step 3’s move once more, , with nothing supposed at all.
Nothing whatever was assumed about , so the logic proves every statement there is; and a logic that proves everything proves nothing, since a proof in it is evidence for nothing. This is Curry’s paradox (1942), and the culprit is the statement that feeds on itself. It is the culprit of §1.2: the one gives a program that never stops, the other a proof of everything, and they are built the same way.
Curry’s is the simple version, and it came second. The inconsistency was found by Kleene and Rosser (1935), who reached it the long way round: they built, inside the calculus, a term that reproduces an old puzzle of Richard’s about naming numbers. The puzzle goes like this. A finite phrase of English can name a real number — “one half”, “the ratio of a circle’s circumference to its diameter” — and the finite phrases themselves can be listed, by length and alphabetically within each length. So the numbers nameable in English can be laid out in order: a first, a second, a third, and so on. Now name a number by this phrase: the number whose th decimal digit differs from the th digit of the th number on the list. That phrase is finite English, so the number it names is itself somewhere on the list, at place say — and it differs from the th number of the list at the th digit, so it is not that number after all.
Richard’s puzzle is about naming, and Church’s calculus is a calculus of naming: a term is a name for a function, and terms can be taken apart and built up by other terms. Once a calculus can name all of its own names, the list and the diagonal are both available inside it, and that is what Kleene and Rosser assembled. Curry’s achievement was to see which single ingredient had done the damage; his paradox needs that one and nothing else. (The diagonal is not a mistake, and it does not go away. It returns in Chapter 11, where it is put to work rather than defused.)
Church’s repair was types (1940): attach a type to every term and forbid an application unless the types fit, which is rule (app). Proposition 1.15 is the repair working. In the typed calculus cannot be written, the self-feeding statement cannot be formed, and the disaster does not occur. It is the repaired calculus, not the original, that your compiler is built around, and that is why the checker’s whole job is to refuse.
Look again at the paragraph in which the logic broke. Statement, suppose, so, discharge, conclude, proves: every word in it is a word about proofs, and we have not said what a proof is. And that is not the only thing pointing the same way. Look at the two types (1.1) and (1.2) give the constants,
Letters and arrows: they are formulas, and logicians have been writing exactly these two for a century, in systems built to say what a proof is. So we have a language in which a program and a logical disaster turned out to be the same object; a checker that refuses both; and two rules whose types a logician would recognise on sight — and no idea yet what it is about those four rules that makes them rules of reasoning. That is the next chapter’s question, and the answer is better than anyone has a right to expect.
Under every language you use is a combinatory algebra: two constants and application, run by two rewrite rules, in which every definition you have ever written can be expressed; and unpoliced it computes everything, including a program that never stops. A type is a contract, a basic type or ; the checker’s job is to allow an application only when the contract is met. Its certificate is a typing: a finite list of judgements , each justified in one of the four ways (K), (S), (var) and (app). Its no is case (app) failing to match, and is refused that way. Its yes mentions no input and so covers every input, and by subject reduction what it says of a program stays said at every step of the program’s run.
Exercises
Exercise 1.1. Let . Run by the rules of Definition 1.3, one step per line, naming each. Then say what computes in general, and why the definition has three parameters where had four.
Exercise 1.2. Run by the two constant rules alone, and confirm that it reaches . How many steps?
Exercise 1.3. Using Definition 1.11:
- (1)
- In the context , give a typing of , the term that runs to after two steps.
- (2)
- Give a typing of the program itself, with the empty context.
Keep both typings; Chapter 2 uses them again.
Exercise 1.4. Consider . Find a run of it that stops, and a run that never does. Does this contradict Remark 1.7?
Exercise 1.5. Give typings of and of . Then run for an arbitrary , and say what is.
Exercise 1.6. Every real language has recursion. Model it by a new constant with the step and the rule , for every type .
Reading
Wadler (2015) tells the story of this chapter and the next two in a dozen pages, and is a pleasure to read. Schönfinkel (1924) is where and come from; Church (1936) and Turing (1937) are where the calculus is shown to compute everything; Church (1940) is where it gets its types, and Kleene and Rosser (1935) and Curry (1942) are why. Tait (1967) proves Theorem 1.17. Next, Chapter 2: what a proof is, and why the compiler’s rules are its rules. References.
- Alonzo Church. A set of postulates for the foundation of logic. Annals of Mathematics, 33(2):346–366, 1932.
- Alonzo Church. An unsolvable problem of elementary number theory. American Journal of Mathematics, 58(2):345–363, 1936.
- Alonzo Church. A formulation of the simple theory of types. Journal of Symbolic Logic, 5(2):56–68, 1940.
- Alonzo Church and J. Barkley Rosser. Some properties of conversion. Transactions of the American Mathematical Society, 39(3):472–482, 1936.
- Haskell B. Curry. The inconsistency of certain formal logics. Journal of Symbolic Logic, 7(3):115–117, 1942.
- Stephen C. Kleene and J. Barkley Rosser. The inconsistency of certain formal logics. Annals of Mathematics, 36(3):630–636, 1935.
- Mars Climate Orbiter Mishap Investigation Board. Phase I report. Technical report, NASA, Washington, DC, November 1999.
- Moses Schönfinkel. Über die Bausteine der mathematischen Logik. Mathematische Annalen, 92:305–316, 1924.
- William W. Tait. Intensional interpretations of functionals of finite type I. Journal of Symbolic Logic, 32(2):198–212, 1967.
- Alan M. Turing. Computability and -definability. Journal of Symbolic Logic, 2(4):153–163, 1937.
- Philip Wadler. Propositions as types. Communications of the ACM, 58(12):75–84, 2015.