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 x,y,z,f,g,…. The terms are defined inductively:

1.
Each constant, 𝖪 and 𝖲, is a term.
2.
Each variable x is a term.
3.
If M and N are terms, then MN is a term: the application of M to N.
4.
Nothing else is a term.

A definition is an equation

fx1⋯xn := M,

where f is a new name, x1,…,xn are distinct variables, and M is a term built from constants, names defined earlier, and the variables x1,…,xn alone. Once defined, the name f 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 M and N standing for terms already built,

M,N ::= 𝖪∣𝖲∣x∣MN.

Application groups to the left: fxy is (fx)y, 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 f(gx). 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 f(gx) of a definition, is a fragment — it says what to do once f, g and x 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:

𝑡𝑤𝑖𝑐𝑒fx := f(fx),𝑐𝑜𝑚𝑝fgx := f(gx),𝑓𝑙𝑖𝑝fxy := fyx.

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, f and x are distinct variables, and the body f(fx) 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 f stands next to n 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:

𝖪MN ⇝M, 𝖲MNP ⇝MP(NP), fM1⋯Mn ⇝M with Mi in place of each xi,

the third whenever fx1⋯xn := M 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 M ⇝∗ N when N can be reached from M 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 0,1,2,… and the operator + as ready-made constants, with the obvious step m + n ⇝ 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:

𝖲𝖪𝖪x ⇝𝖪x(𝖪x) the 𝖲 step, with M = N = 𝖪 and P = x ⇝x the 𝖪 step: keep x, drop 𝖪x.

Whatever x is, 𝖨x runs to x. 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 bMN: the boolean applied to the two branches. Run both cases.

𝑡𝑟𝑢𝑒MN = 𝖪MN ⇝M keep the first 𝑓𝑎𝑙𝑠𝑒MN = 𝖪𝖨MN ⇝𝖨N 𝖪 keeps 𝖨, drops M ⇝N Example 1.4.

There is no if in the language and none is needed: the boolean is the conditional.

Example 1.6 (numerals, and addition). A number n is “do it n times”: a function that takes an f and an x and applies f to x that many times. One definition for each n,

0¯fx := x,1¯fx := fx,2¯fx := f(fx),3¯fx := f(f(fx)),…

To add one to a number, apply f once more; to add two numbers, apply f the first number of times to the result of applying it the second number of times:

𝑠𝑢𝑐𝑐nfx := f(nfx),𝑎𝑑𝑑mnfx := mf(nfx).

Here is 𝑎𝑑𝑑 at work. To see a number come out we need an f that does something visible, so let 𝑖𝑛𝑐y := y + 1 and run 𝑎𝑑𝑑2¯1¯ on 𝑖𝑛𝑐 and 0, one step per line, with the definition unfolded shown on the right:

𝑎𝑑𝑑2¯1¯𝑖𝑛𝑐0 ⇝2¯𝑖𝑛𝑐(1¯𝑖𝑛𝑐0) 𝑎𝑑𝑑 ⇝𝑖𝑛𝑐(𝑖𝑛𝑐(1¯𝑖𝑛𝑐0)) 2¯ ⇝𝑖𝑛𝑐(𝑖𝑛𝑐(𝑖𝑛𝑐0)) 1¯ ⇝𝑖𝑛𝑐(𝑖𝑛𝑐(0 + 1)) 𝑖𝑛𝑐, innermost ⇝𝑖𝑛𝑐(𝑖𝑛𝑐1) ⇝𝑖𝑛𝑐(1 + 1) 𝑖𝑛𝑐, innermost ⇝𝑖𝑛𝑐2 ⇝2 + 1 ⇝3.

Nine steps, and the answer is 3: applied to 𝑖𝑛𝑐 and 0 the program 𝑎𝑑𝑑2¯1¯ computes exactly what 3¯ 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 𝖲𝖡nfx ⇝𝖡f(nf)x ⇝ f(nfx).

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

ωx := xx

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 x, 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 𝖲𝖨𝖨x ⇝∗ xx — 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 A → B as a contract: give me an A and I shall return a B. 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 p,q,r,… for basic types when it does not matter which. The types are defined inductively:

1.
Every basic type p is a type.
2.
If A and B are types, then A → B is a type: the function type, the type of functions that take an A and return a B.
3.
Nothing else is a type.

In one line, A,B ::= p∣A → B. So 𝐼𝑛𝑡 →𝐵𝑜𝑜𝑙 is the type of a test on integers, and (p → q) → p is a type too; look at the shape of that one for a moment before reading on. The arrow groups to the right, so A → B → C is A → (B → C), 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

x1 : A1,…,xn : An

pairing each variable in scope with a type, no variable listed twice; the empty list counts, and I write Γ,x : A for Γ with the pair x : A added at the end. The checker’s basic claim is a judgement

Γ ⊢ M : A,

read “M has type A when its variables have the types Γ gives them”. When Γ is empty I write simply ⊢ M : A, and then M is a program with type A 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

(K)𝖪 : A → B → A (1.1) (S)𝖲 : (A → B → C) → (A → B) → A → C (1.2)

where A, B and C stand for arbitrary types: each line is a schema, and gives one type for each choice of letters. A typing of Γ ⊢ M : A is a finite sequence of judgements

Γ ⊢ M1 : A1,…,Γ ⊢ Mn : An

all carrying the same context Γ, whose last line is Γ ⊢ M : A, and in which each line Γ ⊢ Mk : Ak is justified in one of four ways:

1.
(K) Mk is 𝖪, and Ak is an instance of (1.1);
2.
(S) Mk is 𝖲, and Ak is an instance of (1.2);
3.
(var) Mk is a variable x, and x : Ak is in Γ;
4.
(app) there are two earlier lines i and j, with i,j < k, such that Ai is the type Aj → Ak and Mk is the term MiMj.

No other kind of line is allowed. On the right of each line I write its justification: “(app) i, j” names line i as the function and line j 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 A, then a B, and returns the A, and (1.1) says so for every choice of A and B.

(S)

𝖲 takes a function M of two arguments, a function N of one, and an argument P, and returns MP(NP). For that to make sense P must be what both M and N expect:

P : A,N : A → B,M : A → B → C,

and the result MP(NP) is then a C. 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 x up in Γ.

(app)

An application MN is well typed only if M is a function and N is an argument of exactly the type M expects, and then the result has M’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 y + 1 is + applied to y and then to 1), 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 𝑎𝑑𝑑2¯1¯𝑖𝑛𝑐0 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 A → A. The square brackets name the types substituted for the schema’s letters, in order.

1.
⊢𝖲 : (A → (A → A) → A) → (A → (A → A)) → A → A (S) [A,A → A,A]
2.
⊢𝖪 : A → (A → A) → A (K) [A,A → A]
3.
⊢𝖲𝖪 : (A → (A → A)) → A → A (app) 1, 2
4.
⊢𝖪 : A → (A → A) (K) [A,A]
5.
⊢𝖲𝖪𝖪 : A → A (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 𝖲𝖪𝖪 : A → A, so 𝖲’s C must be A and its A must be A, which leaves its B free; and lines 2 and 4 both go through if B is chosen to be A → A. The search took a sentence; the check takes none.

Example 1.13 (the red line). Now try to type + 3"𝚑𝚎𝚕𝚕𝚘".

1.
⊢ + : 𝐼𝑛𝑡 →𝐼𝑛𝑡 →𝐼𝑛𝑡 ready-made
2.
⊢ 3 : 𝐼𝑛𝑡 ready-made
3.
⊢ +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 A of the function’s A → B, 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 A → A, 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 Γ ⊢𝖲𝖪𝖪 : T then T is U → U for some type U.

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:

Γ ⊢𝖲𝖪 : A′ → T,Γ ⊢𝖪 : A′,Γ ⊢𝖲 : A″ → A′ → T,Γ ⊢𝖪 : A″.

Now put the two schemas against those four lines.

1.
Only case (S) types the constant 𝖲, so A″ → A′ → T has the shape (X → Y → Z) → (X → Y ) → X → Z: that is, A″ = X → Y → Z, A′ = X → Y and T = X → Z.
2.
Only case (K) types the constant 𝖪, and it gives types of the shape V → W → V . The type A″ = X → Y → Z has that shape, which forces Z = X.
3.
The type A′ = X → Y has that shape too, which forces Y to be some W → X.

Hence T = X → Z = X → X. □

Proposition 1.15 (Ω has no type). There is no context Γ and no type T with Γ ⊢𝖲𝖨𝖨 : T. Consequently Ω has no typing.

Proof. Suppose there were, and read the typing backwards as in the lemma. The last line came by (app) from 𝖲𝖨 : A′ → T and 𝖨 : A′, and the first of those from 𝖲 : A″ → A′ → T and 𝖨 : A″. By (S), A″ = X → Y → Z and A′ = X → Y for some X, Y , Z. By Lemma 1.14, both A″ and A′ have the shape U → U: so X = Y → Z from the first and X = Y from the second, and therefore

Y = Y → Z.

No type satisfies that equation. The type Y → Z contains every symbol of Y and an arrow besides, so it is strictly longer than Y , 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 x : A, the body xx of ω would need x : A′ → B and x : A′ at once, and (var) gives x the one type A, so A = A → B. 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 Γ ⊢ M : A and M ⇝ N by a 𝖪 step, an 𝖲 step or a ready-made step, then Γ ⊢ N : A.

Proof. First the case in which the whole of M is the part rewritten, and then the case in which it sits inside something larger.

A 𝖪 step. Then M is 𝖪PQ and N is P. Read the typing of M backwards twice. The line 𝖪PQ : A came by (app) from 𝖪P : B → A and Q : B, and the line 𝖪P : B → A came by (app) from

𝖪 : C → B → AandP : C.

Schema (1.1) gives 𝖪 no types but those of the shape V → W → V , so C = A. Hence Γ ⊢ P : A, which is what we want.

An 𝖲 step. Then M is 𝖲PQR and N is PR(QR). Reading backwards three times gives

R : D,Q : E,P : F,𝖲 : F → E → D → A,

and schema (1.2) forces F = X → Y → Z, E = X → Y , D = X and A = Z. Now build the typing of N forwards:

1.
(app) on P : X → Y → Z and R : X gives PR : Y → Z;
2.
(app) on Q : X → Y and R : X gives QR : Y ;
3.
(app) on those two gives PR(QR) : Z, and Z is A.

A ready-made step. m + n ⇝the sum: both sides have type 𝐼𝑛𝑡.

Inside a larger term. If the part rewritten is a proper subterm of M, 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 B you please — let it be “0 = 1”, if you want something plainly false — and let X be the statement

“if X holds, then B holds.”

That X can be written down at all is the self-application of §1.2 over again: X names the very statement it sits inside, as ω was applied to ω. Now reason about it, in five steps.

1.
Suppose X.
2.
X says that X implies B, so on that supposition we have X → B.
3.
From X (the supposition) and X → B (step 2) we get B.
4.
Steps 1–3 got B out of the supposition X. Discharge it, and conclude X → B — now with nothing supposed.
5.
But X → B is exactly what X says. So we have X outright; and then, by step 3’s move once more, B, with nothing supposed at all.

Nothing whatever was assumed about B, 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 nth decimal digit differs from the nth digit of the nth number on the list. That phrase is finite English, so the number it names is itself somewhere on the list, at place k say — and it differs from the kth number of the list at the kth 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 xx 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,

A → B → Aand(A → B → C) → (A → B) → A → C.

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 A → B; 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 Γ ⊢ M : A, 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 𝑚𝑢𝑙𝑡mnf := m(nf). Run 𝑚𝑢𝑙𝑡2¯3¯𝑖𝑛𝑐0 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 𝖲(𝖪𝖲)𝖪fgx by the two constant rules alone, and confirm that it reaches f(gx). How many steps?

Exercise 1.3. Using Definition 1.11:

(1)
In the context f : B → C,g : A → B, give a typing of 𝖲(𝖪f)g, the term that 𝖡fg 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 𝖪xΩ. 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 𝖲𝖪Mx for an arbitrary M, and say what 𝖲𝖪M is.

Exercise 1.6. Every real language has recursion. Model it by a new constant 𝑓𝑖𝑥 with the step 𝑓𝑖𝑥f ⇝ f(𝑓𝑖𝑥f) and the rule Γ ⊢𝑓𝑖𝑥 : (A → A) → A, for every type A.

(1)
Write a program with a typing that never stops.
(2)
Which of Theorems 1.16 and 1.17 survives the addition, and which does not?
(3)
Read the type of 𝑓𝑖𝑥 as a formula. What does it say, and what does your answer to (1) say about every type A?

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.