Here is how you were taught to prove an implication. To prove “if A then B”, you write suppose A, you reason until you reach B, and you write therefore A implies B, at which point the supposition is spent: it may not be used again, and the conclusion does not depend on it. To use an implication you already have, you write we have A → B, and we have A, so B. And a supposition in force may be written down wherever it is wanted. Three moves, and every proof you have read in a mathematics lecture is built from them and their cousins for the other connectives.

Now take the rule for parameters that Chapter 2 licensed, write it beside (var) and (app), and rub out the programs:

Γ,A ⊢ A Γ ⊢ A → BΓ ⊢ A Γ ⊢ B Γ,A ⊢ B Γ ⊢ A → B

A supposition in force may be written down. From A → B and A, conclude B. If B follows on the supposition A, conclude A → B and drop the supposition. They are the same three moves. The checker that reads your definition f x = ... is checking a proof written the way mathematicians write proofs, and this chapter is about the logic that makes that exact.

3.1 The λ-calculus

In Chapter 1 a definition fx := M had a name, f, and the name had to be declared before it could be used. Church’s notation does away with the name: it writes the function that takes x and returns M as 𝜆𝑥.M, a term in its own right, which can be written wherever a term can be written and applied without ever being named.

Definition 3.1 (λ-terms). Fix a stock of variables x,y,z,f,…. The terms are defined inductively: every variable x is a term; if M and N are terms then MN is a term, the application of M to N; if x is a variable and M is a term then 𝜆𝑥.M is a term, the abstraction of M over x, the function that takes x and returns M; and nothing else is a term. In one line,

M,N ::= x∣MN∣𝜆𝑥.M.

Application groups to the left, as before, and a λ reaches as far to the right as it can, so 𝜆𝑥.MN is 𝜆𝑥.(MN), not (𝜆𝑥.M)N. We keep the ready-made integers and + as constants.

You have written the third form many times. It is \x -> M in Haskell, lambda x: M in Python, x => M in JavaScript; every def f(x): return M is one with a name attached, and every method in Java is one with a name and a class attached. A definition fx1⋯xn := M of Chapter 1 is the term λx1.⋯λxn.M, and the two constants are the two terms

𝖪 := 𝜆𝑥.𝜆𝑦.x,𝖲 := 𝜆𝑥.𝜆𝑦.𝜆𝑧.xz(yz),

which is why they need no longer be taken as given.

Where the notation comes from. The calculus is Alonzo Church’s. He introduced the notation at Princeton in the paper §1.3 described (1932): the λ was there to write the functions of a logic meant to carry the whole of mathematics. The logic did not survive and the notation did. Church used it to exhibit a question no procedure can settle (1936), Turing proved in the same year that it computes exactly what his machines compute, and Church repaired it with types (1940) — the repair your compiler inherited. Schönfinkel’s two combinators had been published in 1924 with the same object in view, and Curry spent the 1930s on them, which is why one language has two origins: the combinatory algebra of Chapter 1 is this calculus with the variables compiled away, and Theorem 3.3 below says that nothing is lost either way. That the calculus is also the heart of every programming language was noticed twenty years later.

Free, bound, and substitution. The x in 𝜆𝑥.M is the function’s parameter: the λ binds it, and its scope is the body M. An occurrence of a variable is bound if it lies in the scope of a λ on that variable and free otherwise. In (𝜆𝑥.xy)x the first two occurrences of x are bound and the last, like the y, is free. The set FV(M) of free variables of M is defined by recursion on the shape of M, one clause for each of the three forms:

1.
M is a variable x: FV(x) = {x}.
2.
M is an application M1M2: FV(M1M2) = FV(M1) ∪FV(M2).
3.
M is an abstraction 𝜆𝑥.M1: FV(𝜆𝑥.M1) = FV(M1) ∖{x}.

The first two clauses only collect; the third is the one that does anything. Only a λ removes a variable from the list, and it removes only its own parameter. A term with no free variables is closed: a whole program rather than a fragment of one. Two terms that differ only in the names of their bound variables, like 𝜆𝑥.x and 𝜆𝑦.y, describe the same function, and we treat them as the same term: a bound variable may be renamed whenever that is convenient.

Substitution M(N∕x) replaces the free occurrences of x in M by N, and nothing else. In Chapter 1 this was simply replacement, because nothing in that language bound a variable; now it has to step around binders, and it too is defined by recursion on the shape of M, one clause per form, except that the variable and abstraction cases each split according to whether the variable met is the very one being replaced:

1.
M is the variable x itself: x(N∕x) = N.
2.
M is a variable y other than x: y(N∕x) = y.
3.
M is an application M1M2: (M1M2)(N∕x) = M1(N∕x)M2(N∕x).
4.
M is an abstraction on x itself: (𝜆𝑥.M1)(N∕x) = 𝜆𝑥.M1.
5.
M is an abstraction on some other variable y:
(𝜆𝑦.M1)(N∕x) = 𝜆𝑦.M1(N∕x),provided y∉FV(N).

Clauses 1 to 3 do what you would expect: replace the variable when you meet it, and otherwise pass the substitution down. Clause 4 stops at a 𝜆𝑥 because every x inside it is bound: there is nothing to replace. The side condition on clause 5 is the one subtlety. Pushing N under a 𝜆𝑦 when y is free in N would turn that free y into a bound one and change what N means, so the clause declines; the remedy is to rename the y of 𝜆𝑦.M1 to a letter that occurs nowhere else, and then substitute. Rename first. Chapter 4 shows what goes wrong when a substitution skips that step, and shows the same bug on the logic side, where it has been known for a century.

Running. There is one rule of computation. To apply a function to an argument, substitute the argument for the parameter:

(𝜆𝑥.M)N ⇝M(N∕x), (3.1)

anywhere inside a term; the pair on the left is a redex, and the step is called β. That is what a function call does, and it is all a function call does. The three steps of Definition 1.3 are all instances of it: the 𝖪 step is two β steps on (𝜆𝑥.𝜆𝑦.x)MN, the 𝖲 step is three, and a definition step is one for each parameter.

Example 3.2 (the same addition, with the parameters visible). Let

𝑎𝑑𝑑 := 𝜆𝑚.𝜆𝑛.𝜆𝑓.𝜆𝑥.mf(nfx), 2¯ := 𝜆𝑓.𝜆𝑥.f(fx), 1¯ := 𝜆𝑓.𝜆𝑥.fx, 𝑖𝑛𝑐 := 𝜆𝑦.y + 1.

The first step of Example 1.6 becomes four:

𝑎𝑑𝑑2¯1¯𝑖𝑛𝑐0 ⇝(𝜆𝑛.𝜆𝑓.𝜆𝑥.2¯f(nfx))1¯𝑖𝑛𝑐0 2¯ for m ⇝(𝜆𝑓.𝜆𝑥.2¯f(1¯fx))𝑖𝑛𝑐0 1¯ for n ⇝(𝜆𝑥.2¯𝑖𝑛𝑐(1¯𝑖𝑛𝑐x))0 𝑖𝑛𝑐 for f ⇝2¯𝑖𝑛𝑐(1¯𝑖𝑛𝑐0) 0 for x,

and from here the run is the one you have seen, each unfolding of a numeral now two β steps and each unfolding of 𝑖𝑛𝑐 one. Nothing has changed but the grain. A λ is a parameter waiting for its argument, and β is the argument arriving.

3.2 Typing

The types are those of Definition 1.9 and the contexts and judgements those of Definition 1.10. Two of the rules are Chapter 1’s, for variables and for applications. The third is the one the Deduction Theorem licensed — if B follows from the hypothesis A, then A → B follows without it (Theorem 2.7) — which we may now adopt outright, since adopting it lets nothing new be typed:

(var) Γ ⊢ x : Aif x : A is in Γ (app)Γ ⊢ M : A → BΓ ⊢ N : A Γ ⊢ MN : B (abs) Γ,x : A ⊢ M : B Γ ⊢ 𝜆𝑥.M : A → B (3.2)

Read (abs) as the checker reads a function definition: to check 𝜆𝑥.M, add the parameter to the context with the type it is to have, check the body, and the function has the arrow type. If Γ already declares an x, rename the parameter first, since a context lists each variable once. Rule (abs) is the first rule we have met that changes the context: above the line there is one more variable in scope than below it. The constants are gone — 𝖪 and 𝖲 are now terms, and they get their types by (abs), which Exercise 3.1 asks you to check gives exactly (1.1) and (1.2). A typing is a finite list of judgements as before, with the conventions for justifications as before; the one difference from Definition 1.11 is that the lines of a single typing no longer all carry the same context.

Theorem 3.3 (λ-terms and combinator terms are one language). Every typed λ-term translates to a combinator term of the same type that runs the same way on every argument, and every typed combinator term is a typed λ-term of the same type.

Proof sketch. Right to left is the display above: write 𝜆𝑥.𝜆𝑦.x for 𝖪 and 𝜆𝑥.𝜆𝑦.𝜆𝑧.xz(yz) for 𝖲; their types come out as (K) and (S) by (abs), and the rules (var) and (app) are shared. Left to right: take the innermost abstraction 𝜆𝑥.M in the term, so that M contains no λ, and run the construction in the proof of Theorem 2.7 on the typing of M in the context x : A, with the programs left on rather than rubbed out. Its first case writes a 𝖪, its second the identity 𝖲𝖪𝖪 and its third an 𝖲, and what comes out is a term without λ, of the same type A → B, which does to its argument what 𝜆𝑥.M does. Repeat until no λ is left. □

So nothing is lost by the change of notation, and the checker of Chapter 1 and the checker of (3.2) accept the same programs at the same types. What is gained is that the compiler no longer has to compile anything to decide whether to say yes: it reads the definition as written.

A worked typing. Here is the typing of a program you have written in one form or another, \f -> \g -> \x -> f (g x), in the context Γ = f : B → C,g : A → B,x : A.

1.
Γ ⊢ g : A → B (var): g : A → B is in Γ
2.
Γ ⊢ x : A (var): x : A is in Γ
3.
Γ ⊢ gx : B (app) 1, 2
4.
Γ ⊢ f : B → C (var): f : B → C is in Γ
5.
Γ ⊢ f(gx) : C (app) 4, 3
6.
f : B → C,g : A → B ⊢ 𝜆𝑥.f(gx) : A → C (abs) 5
7.
f : B → C ⊢ 𝜆𝑔.𝜆𝑥.f(gx) : (A → B) → A → C (abs) 6
8.
⊢ 𝜆𝑓.𝜆𝑔.𝜆𝑥.f(gx) : (B → C) → (A → B) → A → C (abs) 7

Lines 1 to 5 are routine: three look-ups and two uses of (app), each matching an argument’s type against the type a function expects. Lines 6 to 8 are (abs) three times, each taking the last variable out of the context and putting a λ on the front of the program: watch the context shrink as you read down, until at line 8 it is empty and the program is closed. This is what the compiler did, in a few microseconds, when it accepted your composition function.

Rub out the programs. Lines 1–5 are a proof of C from the three hypotheses B → C, A → B and A: three hypothesis lines and two uses of modus ponens. That is hypothetical syllogism — from A → B and B → C, conclude A → C — which Chapter 2 proved from hypotheses in seven lines. Lines 6–8 discharge A, then A → B, then B → C, and the formula on the last line,

(B → C) → (A → B) → A → C,

is hypothetical syllogism as a theorem, with nothing assumed. That formula cost Chapter 2 seven lines of which the first was an instance of (II) with twenty letters and nineteen arrows; here it is found and checked in three discharges. The compiler read your program as that proof.

3.3 Conjunction, disjunction, and negation

The arrow is one connective, and a logic needs and, or and not as well. Each is a type you have used — a pair, a tagged union, the empty type — and each comes with its rules the way → did. A rule that introduces a connective is one whose conclusion has that connective on the outside; on the program side it builds a value of the type, so it is a constructor. A rule that eliminates one has it on the outside of a premise; on the program side it takes a value apart, so it is a pattern match. Those two words, introduction and elimination, will organise everything that follows.

Definition 3.4 (conjunction). If A and B are formulas, so is A ∧ B; as a type it is the pair type, (A, B) in Haskell. The rules, with their terms:

(∧I)Γ ⊢ M : AΓ ⊢ N : B Γ ⊢ (M,N) : A ∧ B (∧E1)Γ ⊢ M : A ∧ B Γ ⊢𝑓𝑠𝑡M : A (∧E2)Γ ⊢ M : A ∧ B Γ ⊢𝑠𝑛𝑑M : B

To establish A ∧ B you establish both; to use it you take whichever half you need. A proof of a conjunction is a pair of proofs, and the two eliminations are the two projections.

Definition 3.5 (disjunction). If A and B are formulas, so is A ∨ B; as a type it is Either A B. The rules:

(∨I1) Γ ⊢ M : A Γ ⊢𝐿𝑒𝑓𝑡M : A ∨ B(∨I2) Γ ⊢ M : B Γ ⊢𝑅𝑖𝑔h𝑡M : A ∨ B
(∨E)Γ ⊢ M : A ∨ BΓ,x : A ⊢ P : CΓ,y : B ⊢ Q : C Γ ⊢𝐜𝐚𝐬𝐞M𝐨𝐟𝐿𝑒𝑓𝑡x ⇒ P∣𝑅𝑖𝑔h𝑡y ⇒ Q : C

To establish A ∨ B you establish one of them and say which. To use it you argue by cases: if you can reach C supposing A, and reach C supposing B, then you have C — and both suppositions are discharged, like the supposition in ( →I). A proof of a disjunction is a proof of one side with a tag saying which, and the elimination is a case expression with one branch per tag.

Definition 3.6 (falsity, and negation). ⊥ is a formula, read false; as a type it is the type with no constructor, data Void in Haskell, of which there are no values. It has no introduction rule and one elimination:

(⊥E) Γ ⊢ M : ⊥ Γ ⊢𝑎𝑏𝑠𝑢𝑟𝑑M : C

for every formula C. Negation is an abbreviation: ¬ ⁡A is A →⊥.

There is no way to establish ⊥ outright, and that is the point of it. If you ever have ⊥ in hand you may conclude anything, which is what “from a contradiction, anything follows” means and what absurd :: Void -> a says; and ¬ ⁡A is the claim that from A you could reach ⊥, a function from A to the empty type. Everything in this part that looks like a negation is an implication into ⊥, and the rules for ¬ ⁡ are the rules for →. (Whether ⊥ can be established from some set of hypotheses is a different question, and it is the question of §3.6.) Here is the whole language in one table: each connective, the type it is, how a value of that type is built, and how it is taken apart.

Connective

Type

Introduction
(constructor)

Elimination
(pattern match)

A →B

A -> B

𝜆𝑥.M

application MN

A ∧B

(A, B)

(M,N)

𝑓𝑠𝑡, 𝑠𝑛𝑑

A ∨B

Either A B

𝐿𝑒𝑓𝑡M, 𝑅𝑖𝑔h𝑡M

case with two branches

⊥

Void

—

𝑎𝑏𝑠𝑢𝑟𝑑

¬ ⁡A

A -> Void

𝜆𝑥.M

application

That is the table of the part, and the thing to carry away from it: the connectives of logic are the data types of a programming language, the introduction rules are their constructors, and the elimination rules are the pattern matches that take them apart. A proof is a program that builds and takes apart values of these types, and nothing else.

Example 3.7 (three small programs). Each of the following is a theorem, and here is the program that proves it.

1.
A ∧ B → B ∧ A: the program is 𝜆𝑝.(𝑠𝑛𝑑p,𝑓𝑠𝑡p). In the context p : A ∧ B, (∧E2) gives 𝑠𝑛𝑑p : B and (∧E1) gives 𝑓𝑠𝑡p : A; (∧I) gives the pair the type B ∧ A; (abs) discharges p.
2.
Contraposition:
(A → B) → (¬ ⁡B →¬ ⁡A).

The program is 𝜆𝑓.𝜆𝑘.𝜆𝑎.k(fa). In the context f : A → B,k : B →⊥,a : A, two uses of (app) give k(fa) : ⊥, and three uses of (abs) give the type claimed, reading ¬ ⁡B as B →⊥ and ¬ ⁡A as A →⊥. Chapter 4 uses this one as a line in a proof.

3.
A →¬ ⁡¬ ⁡A: the program is 𝜆𝑎.𝜆𝑘.ka. In the context a : A,k : A →⊥, (app) gives ka : ⊥, and two uses of (abs) give A → (A →⊥) →⊥.

In each case the program is one you could have written without knowing what it proved: a function that swaps a pair; a function that composes a test with a function; a function that hands its first argument to its second. The types were there all along.

3.4 Natural deduction

Everything so far has been about programs. Now rub the programs out. Take the rules of (3.2) and §3.3, delete every term, and keep what is left: judgements Γ ⊢ A relating a set of formulas to a formula. Ten rules survive, and here they are.

Definition 3.8 (natural deduction). A derivation of A from a set of formulas Γ is a finite list of judgements Δ ⊢ C, each justified by one of the following rules from judgements earlier in the list, ending in Γ ⊢ A.

(hyp) Γ ⊢ Aif A ∈Γ ( →E)Γ ⊢ A → BΓ ⊢ A Γ ⊢ B ( →I) Γ,A ⊢ B Γ ⊢ A → B (∧E1)Γ ⊢ A ∧ B Γ ⊢ A (∧E2)Γ ⊢ A ∧ B Γ ⊢ B (∧I)Γ ⊢ AΓ ⊢ B Γ ⊢ A ∧ B (∨I1) Γ ⊢ A Γ ⊢ A ∨ B(∨I2) Γ ⊢ B Γ ⊢ A ∨ B (∨E)Γ ⊢ A ∨ BΓ,A ⊢ CΓ,B ⊢ C Γ ⊢ C (⊥E)Γ ⊢⊥ Γ ⊢ C (3.3)

I write Γ ⊢NA when a derivation exists, to keep it apart from the ⊢ of Chapter 2 until Theorem 3.10 has shown that the two agree.

Every one of those rules is a rule you have already used, with the program deleted: ( →I) is a λ, ( →E) an application, (∧I) a pair, (∨E) a case, (⊥E) the function absurd. Nothing has been added. What has changed is only what we are looking at.

And what we are looking at is not a system invented for this book, nor one that came out of computing at all. It is Gerhard Gentzen’s, published in his doctoral thesis (1934), two years before Turing’s machine and before there was a program in the world to type. He called it natural deduction because he built it by watching: he took proofs as mathematicians actually write them and wrote down the moves they actually make. To prove an implication, suppose the antecedent. To use one, apply it. To prove a conjunction, prove both halves. To use a disjunction, argue by cases. Each connective gets rules that say how to prove a statement with that connective on the outside and how to use one — and nothing else. That is the whole design, and the rules of (3.3) are the record of it.

Example 3.9 (the two schemas, derived). Natural deduction has no axioms, so if it is to prove what the Hilbert system proves it must derive (I) and (II). It does, and the derivations are short. For (I):

1.
A,B ⊢ A (hyp)
2.
A ⊢ B → A ( →I) 1
3.
⊢ A → B → A ( →I) 2

For (II), with Γ = {A → B → C,A → B,A}:

1.
Γ ⊢ A → B → C (hyp)
2.
Γ ⊢ A (hyp)
3.
Γ ⊢ B → C ( →E) 1, 2
4.
Γ ⊢ A → B (hyp)
5.
Γ ⊢ B ( →E) 4, 2
6.
Γ ⊢ C ( →E) 3, 5
7.
A → B → C,A → B ⊢ A → C ( →I) 6
8.
A → B → C ⊢ (A → B) → A → C ( →I) 7
9.
⊢ (A → B → C) → (A → B) → A → C ( →I) 8

Put the programs back on the second derivation — x, z, y for the three hypotheses — and line 6 is xz(yz) and line 9 is 𝜆𝑥.𝜆𝑦.𝜆𝑧.xz(yz), which is 𝖲. The first derivation decorated is 𝖪. The two constants of Chapter 1 are the two schemas, derived.

Theorem 3.10 (natural deduction and the Hilbert system prove the same things). Let Γ be a set of formulas and A a formula, all built from letters by → alone — the fragment Chapter 2’s system can express. Then Γ ⊢NA if and only if Γ ⊢ A.

Proof. Right to left: go down a Hilbert proof from Γ and replace each line by a derivation. An instance of (I) or (II) is replaced by its derivation from Example 3.9 (with the instance’s formulas for A, B, C, and with Γ added to every context, which (hyp), ( →E) and ( →I) all permit); a hypothesis line by one use of (hyp); and a modus ponens by one use of ( →E), which is the same rule under another name. Left to right: go down a derivation and replace each line by a Hilbert proof. A (hyp) line is a one-line proof (reflexivity, Proposition 2.6). An ( →E) line is a modus ponens on the two proofs already written. An ( →I) line concludes Γ ⊢ A → B from Γ,A ⊢ B, for which a Hilbert proof has already been written, and the Deduction Theorem (Theorem 2.7) rewrites that into a Hilbert proof of A → B from Γ alone. □

So the two systems are one logic in two notations, and the Deduction Theorem is precisely the thing that makes them so: it is the ( →I) rule of natural deduction, proved admissible for the Hilbert system. From here on I write ⊢ for both.

Theorem 3.11 (Curry–Howard). Derivations of A from Γ in natural deduction and typings of λ-terms M with Γ ⊢ M : A are the same lists: erase the terms from a typing and (var), (app), (abs) become (hyp), ( →E), ( →I); decorate a derivation with terms — a distinct variable for each hypothesis, an application at each ( →E), a λ at each ( →I) — and it becomes a typing.

Proof. Put (3.2) beside (3.3). Rule for rule, the second display is the first with the terms removed; the contexts, the premises and the conclusions are otherwise identical. Erasure and decoration are therefore the relabelling of justifications of Theorem 2.4, with one more case, and the proof is the same walk down the list. □

Haskell Curry noticed in the 1950s that the types of his combinators were the axioms of a Hilbert system (Curry and Feys, 1958); William Howard, in a manuscript of 1969, saw that the same held of the λ-calculus and natural deduction, rule for rule, and that running a program corresponded to something Gentzen had already studied (Howard, 1980). The observation is called the Curry–Howard correspondence. It is the reason this chapter could be written twice over without saying anything twice, and §3.6 takes up the last part of it: what Gentzen had studied, and why he cared.

3.5 Derivations as trees

Definition 3.8 writes a derivation as a list, and carries the set Γ along on every line. That is the right thing for a checker: each line can be checked on its own, because everything it depends on is written on it, and the programs of §3.2 can be written beside the lines. It is the wrong thing for a reader. The hypotheses are copied onto every line whether they are used there or not, and the shape of the argument — which step feeds which — has to be reconstructed from the justifications on the right. Gentzen wrote derivations the other way, as trees, and so does every book since. The hypotheses sit at the leaves, at the top, written once each. Each rule is a horizontal bar with its premises above and its conclusion below, labelled on the right with the rule used. The conclusion of the whole derivation is the single formula at the root, at the bottom. You read a tree downwards, and the shape of the tree is the shape of the argument.

The one thing a tree must record that the list records in its contexts is discharge. The rule ( →I) proves A → B by using A as a hypothesis and then giving it up: after the rule has fired, the conclusion no longer depends on A. So a leaf of a tree is in one of two states. It is open, and the conclusion rests on it; or it has been discharged by some rule below it, and the conclusion does not. A discharged leaf is written in square brackets. Which hypotheses are open is not decoration: it is the whole content of the turnstile, and the definition has to track it.

Definition 3.12 (derivations). The derivations are defined inductively, and with each derivation 𝒟 we define at the same time two things: its conclusion, the formula at the root, and its set of open hypotheses OH(𝒟), a finite set of formulas. In the clauses below, a derivation is drawn as a name with its conclusion written underneath it, and a formula in square brackets above a derivation is one that the clause discharges.

1.
(hypothesis) For every formula A, the one-node tree whose only node is labelled A is a derivation, with conclusion A and OH = {A}.
2.
( →I) If 𝒟 has conclusion B, then for every formula A,
[A] 𝒟 B A → B →Iis a derivation,OH = OH(𝒟)∖{A}.
3.
( →E) If 𝒟1 has conclusion A → B and 𝒟2 has conclusion A, then
𝒟1 A → B 𝒟2 A B →Eis a derivation,OH = OH(𝒟1)∪OH(𝒟2).
4.
(∧I) If 𝒟1 concludes A and 𝒟2 concludes B, then
𝒟1 A 𝒟2 B A ∧ B ∧Iis a derivation,OH = OH(𝒟1)∪OH(𝒟2).
5.
(∧E) If 𝒟 concludes A ∧ B, then
𝒟 A ∧ B A ∧E1and 𝒟 A ∧ B B ∧E2

are derivations, each with OH = OH(𝒟).

6.
(∨I) If 𝒟 concludes A, then for every formula B the tree with root A ∨ B and immediate subtree 𝒟 is a derivation, and symmetrically from a derivation of B; in both cases OH = OH(𝒟).
7.
(∨E) If 𝒟0 concludes A ∨ B, and 𝒟1 and 𝒟2 both conclude C, then
𝒟0 A ∨ B [A] 𝒟1 C [B] 𝒟2 C C ∨E

is a derivation, with OH = OH(𝒟0) ∪ (OH(𝒟1) ∖{A}) ∪ (OH(𝒟2) ∖{B}).

8.
(⊥E) If 𝒟 concludes ⊥, then for every formula C the tree with root C and immediate subtree 𝒟 is a derivation, with OH = OH(𝒟).
9.
Nothing else is a derivation.

We write Γ ⊢ A when some derivation has conclusion A and OH ⊆Γ.

Three remarks on reading that definition, each about a place where students expect something other than what it says.

Discharge takes every open leaf at once. Clause 2 removes A from a set. Every open leaf labelled A in 𝒟 is therefore discharged together, which is why the second tree below can close two copies of A ∧ B with one bracket. Some books let a rule discharge a chosen subset of the occurrences instead; nothing provable changes, because an undischarged copy can always be discharged by a later ( →I) that reintroduces the same formula.

Discharge may be vacuous. Clause 2 does not require A to occur in 𝒟 at all. If it does not, OH(𝒟) ∖{A} is just OH(𝒟), and the rule has proved A → B from a proof of B that never looked at A. That is schema (I) of Chapter 2, and on the program side it is 𝖪: a function that ignores its argument.

The numbers are bookkeeping. The definition says which leaves a clause discharges, so it needs no numbers. They appear in practice because a drawn tree may contain several rules that discharge, and the reader needs to see which bracket belongs to which bar. Writing [A]1 at the leaf and →I,1 at the bar is a labelling of the same information, not more of it.

Three trees. Here is hypothetical syllogism, which §3.4 gave as a six-line list with Γ = {p → q,q → r} repeated on every line. As a tree it is three bars, and the two hypotheses are written once each; they stay open, because the conclusion depends on them.

[p]1p → q q →Eq → r r →E p → r →I,1

Read it from the leaves down: suppose p; with p → q, get q; with q → r, get r; discharge the supposition and conclude p → r. Its open hypotheses are {p → q,q → r}, computed by clause 3 twice and clause 2 once, so the tree witnesses {p → q,q → r}⊢ p → r.

Next, one hypothesis discharged at two leaves at once, which is clause 2 working on a set. The conclusion is ⊢ (A ∧ B) → (B ∧ A), with no open hypotheses at all.

[A ∧ B]1 B ∧E2[A ∧ B]1 A ∧E1 B ∧ A ∧I (A ∧ B) → (B ∧ A) →I,1

On the program side that derivation is 𝜆𝑥.(𝑠𝑛𝑑x,𝑓𝑠𝑡x), and the two bracketed leaves are the two occurrences of the variable x in its body. A variable used twice is a hypothesis discharged at two leaves; a variable used once, at one; and a parameter never used at all is a vacuous discharge.

Last, clause 7, which discharges two different hypotheses, one in each branch — the case where the bracket notation earns its keep. The conclusion is ⊢ (A ∨ B) → (B ∨ A).

[A ∨ B]1 [A]2 B ∨ A∨I2 [B]3 B ∨ A∨I1 B ∨ A ∨E,2,3 (A ∨ B) → (B ∨ A) →I,1

That is argument by cases, written out: we have A ∨ B; in the case A we reach B ∨ A, in the case B we reach B ∨ A; so we have it either way. The program is 𝜆𝑥.𝐜𝐚𝐬𝐞x𝐨𝐟𝐿𝑒𝑓𝑡a ⇒𝑅𝑖𝑔h𝑡a∣𝑅𝑖𝑔h𝑡b ⇒𝐿𝑒𝑓𝑡b, and the two discharged leaves are the two branch variables a and b, each in scope only in its own branch — which is exactly what “one in each branch” means.

Theorem 3.13 (the two notations agree). For every set of formulas Γ and every formula A: some derivation in the sense of Definition 3.12 has conclusion A and open hypotheses inside Γ if and only if some list in the sense of Definition 3.8 derives Γ ⊢ A.

Proof. Left to right, by induction on the derivation 𝒟 in the sense of Definition 3.12. Take as the induction hypothesis that each immediate subderivation has already been turned into a list whose last judgement is OH ⊢ its conclusion. A one-node tree becomes the one-line list “{A}⊢ A, (hyp)”. For each other clause, concatenate the lists of the subderivations, weaken every judgement in them to the context the clause produces — permitted because Definition 3.8’s rules are stated for an arbitrary Γ — and add one line, justified by the matching rule of (3.3). The clause’s equation for OH is exactly the context that rule produces: for ( →I) the set shrinks by A on both sides, and for (∨E) by A in one branch and B in the other.

Right to left, by induction on the length of the list. Each line Δ ⊢ C is replaced by a tree with conclusion C and open hypotheses contained in Δ, built from the trees already made for the lines it cites by the clause of Definition 3.12 matching its justification. A (hyp) line gives a one-node tree. Finally, if the last line is Γ ⊢ A then the tree built for it has conclusion A and open hypotheses inside Γ. □

Induction over derivations. The definition buys more than a notation. Because it is inductive — nine clauses and a “nothing else”, exactly as Definition 1.1 built terms — it comes with a principle of proof, and it is the principle every theorem in the rest of this chapter uses.

To show that every derivation has a property P, it is enough to show that each one-node derivation has P, and that for each of clauses 2–8, if the subderivations the clause uses have P then so does the derivation the clause builds.

That is rule induction, and it is the same move as induction on the natural numbers with the clauses in place of zero and successor. It is how Theorem 3.15 will show that running a program preserves its type, and how Lemma 3.18 will show that a stopped program is a constructor: in each case one checks the base and then walks once through the list of clauses. When you meet “by induction on the derivation” in any book on proof theory, this is what is meant.

We shall go on writing lists, because the checker does and because the programs can be written beside them, and we shall draw trees when the shape of an argument is the point. They are the same object described twice, and Theorem 3.13 is the licence to move between them without comment.

3.6 Normalisation

What Gentzen had studied, and what Howard saw running a program corresponded to, is this. A proof can take a detour: it can establish A ∧ B by (∧I) from proofs of A and B, and then at once take A back out by (∧E1); or establish A → B by ( →I), discharging a supposition, and at once use it on a proof of A by ( →E). In each case an introduction is met by its own elimination, and the proof would have been shorter without the pair. On the program side the pair is a redex, and removing it is a step of computation.

Definition 3.14 (running, for every connective). A step rewrites, anywhere inside a term, an introduction met by its elimination:

(𝜆𝑥.M)N ⇝M(N∕x) 𝑓𝑠𝑡(M,N) ⇝M, 𝑠𝑛𝑑(M,N) ⇝N, 𝐜𝐚𝐬𝐞(𝐿𝑒𝑓𝑡M)𝐨𝐟𝐿𝑒𝑓𝑡x ⇒ P∣𝑅𝑖𝑔h𝑡y ⇒ Q ⇝P(M∕x), 𝐜𝐚𝐬𝐞(𝑅𝑖𝑔h𝑡M)𝐨𝐟𝐿𝑒𝑓𝑡x ⇒ P∣𝑅𝑖𝑔h𝑡y ⇒ Q ⇝Q(M∕y)

There is no rule for 𝑎𝑏𝑠𝑢𝑟𝑑, because ⊥ has no introduction for it to meet. A term in which no step can be taken is in normal form, and a proof with no detour is a normal proof.

These are the steps your interpreter takes: call a function, project a pair, branch on a tag. That they are also the removal of detours from proofs is the content of the correspondence, and the two theorems of Chapter 1 say the same two things about them, now for the whole language.

Theorem 3.15 (subject reduction). If Γ ⊢ M : A and M ⇝ N, then Γ ⊢ N : A.

Proof sketch. The one new ingredient is a substitution lemma: if Γ,x : A ⊢ M : B and Γ ⊢ N : A, then Γ ⊢ M(N∕x) : B. It is proved by going down the typing of M and replacing every line that types x by (var) with the typing of N; every later line is unchanged, since the rules look only at types. With that in hand, each step is one case. A β step: (𝜆𝑥.M)N : B came by (app) from 𝜆𝑥.M : A → B and N : A, and the first of these by (abs) from Γ,x : A ⊢ M : B; the lemma gives M(N∕x) : B. A projection: 𝑓𝑠𝑡(M,N) : A came from (M,N) : A ∧ B, which came from M : A. A case on 𝐿𝑒𝑓𝑡M: the conclusion C came from 𝐿𝑒𝑓𝑡M : A ∨ B, so M : A, and from Γ,x : A ⊢ P : C; the lemma gives P(M∕x) : C. A step inside a larger term replaces the lines for the subterm by lines of the same type, as in Theorem 1.16. □

Theorem 3.16 (normalisation; on loan). Every typed term runs to a normal form. Equivalently: every proof can be rewritten, by removing detours, into a normal proof of the same formula from the same hypotheses.

The program-side proof is Tait’s (1967) and the proof-side proof is Prawitz’s (1965); the two are the same proof, and Girard et al. (1989, chapter 4) give it in a form readable at the level of these notes, for → and ∧, with ∨ and ⊥ needing a little more. We take it on trust, as we did in Chapter 1.

It is worth saying why Gentzen wanted this theorem, because he did not want it in order to run anything. He wanted consistency. In the 1920s Hilbert had set the programme of proving, by arguments so elementary that no one could doubt them, that mathematics could never derive a contradiction; and the elementary arguments allowed were arguments about the shapes of written symbols — no appeal to infinite collections, no appeal to what the symbols were about. A normalisation theorem is exactly such an argument, and it pays off at once. A proof with no detours in it never invents material: every formula appearing in it is a piece of the formulas at its two ends. So if you want to know whether ⊥ can be proved from nothing, you need only ask what a detour-free proof of ⊥ from nothing could possibly look like, and the answer is that there is no shape available for it. Consistency becomes a statement about the grammar of proofs rather than about mathematics, which is why Gentzen proved his version of this theorem — the Hauptsatz, the main theorem of his thesis — before he proved anything with it, and why Theorem 3.19 below costs us a page rather than a book.

How far that method can be pushed is the question of Chapter 11, where it meets a limit that Hilbert did not expect. For now we collect what normalisation buys here, which is a great deal, because a program that has stopped has a very particular shape.

Definition 3.17 (normal and neutral terms). A typed term is neutral if it is a variable, or an elimination applied to a neutral term: NM, 𝑓𝑠𝑡N, 𝑠𝑛𝑑N, 𝐜𝐚𝐬𝐞N𝐨𝐟…, or 𝑎𝑏𝑠𝑢𝑟𝑑N, with N neutral and the other parts in normal form. A term is in normal form, in the sense of Definition 3.14, exactly when it is an introduction form — 𝜆𝑥.M, (M,N), 𝐿𝑒𝑓𝑡M or 𝑅𝑖𝑔h𝑡M — whose parts are in normal form, or is neutral.

The “exactly when” is a small induction on the term, and the one thing it needs is types: a normal-form application MN has an M of arrow type that is not a λ, and the only normal-form terms of arrow type that are not λs are neutral ones; likewise a projection of a non-pair, a case on a non-tag, and an 𝑎𝑏𝑠𝑢𝑟𝑑 of anything, since ⊥ has no introduction form at all. Exercise 3.8 asks for the cases in full. What matters is this: a neutral term is a chain of eliminations hanging off a variable, and that variable is free in it, since nothing in the chain binds it.

Lemma 3.18 (the shape of a stopped program). A closed typed term in normal form is an introduction form. Consequently there is no closed term in normal form whose type is a letter p, and none whose type is ⊥.

Proof. By Definition 3.17 a normal-form term is an introduction form or neutral, and a neutral term has a free variable, its head; so a closed one is an introduction form. An introduction form has type A → B, A ∧ B or A ∨ B by the rule that introduces it, and none of those is a letter or ⊥. □

Theorem 3.19 (consistency). There is no closed term of type ⊥: ⊬ ⊥. More generally, for every letter p, ⊬ p.

Proof. Suppose ⊢ M : ⊥. By Theorem 3.16, M runs to a normal form N. By Theorem 3.15, ⊢ N : ⊥; and N is closed, since no step of Definition 3.14 creates a free variable. So N is a closed normal-form term of type ⊥, which Lemma 3.18 says does not exist. The argument for a letter p is the same with p for ⊥. □

Read what has been shown. The logic of this chapter does not prove everything. It does not prove ⊥, and it does not prove any bare claim p: whatever it proves has an arrow, an and or an or on the outside, which is to say that it proves things about the relationships between claims and never a claim out of nothing. The disaster of §1.3 — a logic in which every statement had a proof — cannot happen here, and the reason it cannot is that every program stops and a stopped program is a constructor. The argument used nothing about what the letters mean, no table, no notion of a formula being the case. It is an argument about the shapes of strings, and it was available to Gentzen in 1934. The same question — does this system prove everything? — asked of arithmetic rather than of the bare connectives, is the subject of Chapter 11, and the answer there is of a different kind.

One more thing falls out of the shape lemma, and it is worth having now.

Corollary 3.20 (a proof of or says which). If ⊢ A ∨ B, then ⊢ A or ⊢ B.

Proof. A closed term of type A ∨ B runs to a closed normal form of that type, which by Lemma 3.18 is an introduction form: 𝐿𝑒𝑓𝑡M with ⊢ M : A, or 𝑅𝑖𝑔h𝑡M with ⊢ M : B. □

In this logic you cannot establish “A or B” without establishing one of them and saying which, because a proof of a disjunction is a value with a tag on it. Hold on to that; Part 2 has a use for it.

3.7 Curry’s paradox

Now the argument that closes §1.3, written out exactly, with the programs on. It needs one thing the rules do not allow, and seeing where the rules refuse is the point.

Example 3.21 (Curry’s paradox, with its program). Suppose, for the length of this example, that there were a type X satisfying the equation

X = X → B,

for some formula B chosen in advance, so that a term of type X could also be read as a function from X to B and the other way round. Then:

1.
x : X ⊢ x : X (var)
2.
x : X ⊢ x : X → B (var), reading X as X → B
3.
x : X ⊢ xx : B (app) 2, 1
4.
⊢ 𝜆𝑥.xx : X → B (abs) 3
5.
⊢ 𝜆𝑥.xx : X line 4, reading X → B as X
6.
⊢ (𝜆𝑥.xx)(𝜆𝑥.xx) : B (app) 4, 5

Rub out the programs and read the justifications: suppose X; then X → B, since that is what X says; so B; discharge the supposition and conclude X → B; which is X; so B. It is the argument of §1.3 line for line, with ( →I) for “discharge”, and since B was arbitrary it proves everything. And the program on line 6 is Ω. The looping program of Chapter 1 and the proof of everything are one term, as §1.3 promised they would be.

Now look at where it fails. Lines 2 and 5 are justified by no rule: they rest on the equation X = X → B, and Proposition 1.15 said why no type satisfies it — a type is a finite tree, and X → B is strictly larger than X. Take the equation away and line 2 cannot be written, so line 3 cannot, so 𝜆𝑥.xx has no type and Ω has none, which is what the proposition said. The one fact that refused the looping program is the one fact that refuses the paradox.

Remark 3.22 (what recursion costs). Every language you use has recursion, and Exercise 1.6 modelled it by a constant 𝑓𝑖𝑥 with the type (A → A) → A for every A and the step 𝑓𝑖𝑥f ⇝ f(𝑓𝑖𝑥f). That constant is exactly what Example 3.21 needed: it lets a program feed on itself without an equation between types. With it, 𝑓𝑖𝑥(𝜆𝑥.x) has every type A there is, ⊥ and every letter included, so Theorem 3.19 fails; and it runs for ever, so Theorem 3.16 fails with it. The two theorems go together, as they came together. Read as a logic, a language with general recursion proves everything, and a type in such a language promises only what the program does if it returns. That is not a reason to give up recursion. It is the reason the checker’s yes, in the languages you use, means “safe” and never “correct”, and the next chapter is about the gap between the two.

The λ-calculus with pairs, sums and the empty type is a proof system — Gentzen’s natural deduction — with the programs written beside the lines: a hypothesis is a variable, ( →I) is λ, ( →E) is application, and the other connectives are your data types, their introduction rules the constructors and their elimination rules the pattern matches. It proves exactly what the Hilbert system proves, and the Deduction Theorem is its ( →I). Running a program is removing detours from a proof, and what the checker says survives it (subject reduction). Every typed program stops (on loan), and a stopped closed program is a constructor; so the logic proves no letter and no ⊥ — it does not prove everything — and a proof of or always says which. Curry’s paradox and Ω are one term, refused by one fact: a type is not a part of itself.

Safe is not correct. Nothing we care about — a sorted list, a transitive order, a discount on every day of the sale — is proved from no hypotheses; by Theorem 3.19 nothing with a bare claim at the top ever is. Such things are proved from a specification, a set of sentences about lists, orders or sales taken as hypotheses. And a specification says for all — for all x, for all y — which no formula of this chapter can say.

3.8 From pseudocode to combinators

This part has three languages in it: the two constants of Chapter 1, the λ-calculus of this one, and the proofs that both turned out to be. They are one language, and the way to believe that is to watch something ordinary travel the whole distance. Here is factorial, as you would write it on a whiteboard.

fact(n): 
    if n = 0 then return 1 
    else return n * fact(n - 1)

Four things in it are not in the language of Definition 1.1: a parameter, a name that calls itself, a conditional, and arithmetic. Take them away one at a time.

The parameter becomes a λ. A definition with a parameter is an abstraction, which is what §3.1 was for. Writing 𝑖𝑠𝑍𝑒𝑟𝑜, 𝑚𝑢𝑙𝑡 and 𝑝𝑟𝑒𝑑 for the test, the product and the predecessor, and using a boolean as its own conditional in the manner of Example 1.5, so that “if b then M else N” is written bMN:

𝑓𝑎𝑐𝑡 := 𝜆𝑛.𝑖𝑠𝑍𝑒𝑟𝑜n1(𝑚𝑢𝑙𝑡n(𝑓𝑎𝑐𝑡(𝑝𝑟𝑒𝑑n))).

Nothing has really happened yet, because 𝑓𝑎𝑐𝑡 still occurs on the right-hand side, and a term may not refer to itself.

The self-reference becomes a parameter. So make it one. Abstract over the recursive call as well, and the self-reference disappears:

F := 𝜆𝑟.𝜆𝑛.𝑖𝑠𝑍𝑒𝑟𝑜n1(𝑚𝑢𝑙𝑡n(r(𝑝𝑟𝑒𝑑n))).

The term F mentions nothing but its own two parameters: give it the function to call and a number, and it does one layer of the work. What we want is a term 𝑓𝑎𝑐𝑡 with 𝑓𝑎𝑐𝑡 = F𝑓𝑎𝑐𝑡 — a fixed point of F — and that is exactly what the constant 𝑓𝑖𝑥 of Exercise 1.6 delivers, since 𝑓𝑖𝑥F ⇝ F(𝑓𝑖𝑥F). So

𝑓𝑎𝑐𝑡 := 𝑓𝑖𝑥F,

and one step shows why that is the right definition: 𝑓𝑖𝑥F3¯ ⇝ F(𝑓𝑖𝑥F)3¯, whose r is 𝑓𝑖𝑥F again — the recursive call is the fixed point unfolding once more, on demand and no sooner.

The arithmetic was combinators already. The three operations and the numerals are the ready-made constants of Chapter 1, and Theorem 1.8 says each of them is a term in 𝖪 and 𝖲: Example 1.6 builds the numerals and addition, Exercise 1.1 multiplication, and the predecessor is a famous enough puzzle to have its own anecdote. Nothing is being smuggled in; we keep them as names only to keep the term on the page.

The λs become 𝖪 and 𝖲. One construction remains, and we already have it. The proof of the Deduction Theorem, run with the programs left on rather than rubbed out, takes a term with a parameter and returns one without (Theorem 3.3). Written as a recipe, it eliminates a single λ from a term M containing no other, producing a term [x]M in which x does not occur:

[x]M = 𝖪M if x does not occur in M, [x]x = 𝖲𝖪𝖪, [x](MN) = 𝖲([x]M)([x]N)otherwise. (3.4)

The three clauses are the three cases of that proof wearing programs: the first is the case of a line that does not use the hypothesis, and writes the 𝖪 that weakens it in; the second is the case of the hypothesis itself, whose proof of A → A is 𝖲𝖪𝖪; the third is the case of a modus ponens, and writes the 𝖲 that pushes the hypothesis through it. The recipe works because of the guarantee that comes with it: ([x]M)N runs to M with N in place of x, for every N.

Apply it to F, innermost parameter first. Write B for the body, so that F = 𝜆𝑟.𝜆𝑛.B, and read B as the application (𝑖𝑠𝑍𝑒𝑟𝑜n1)(𝑚𝑢𝑙𝑡n(r(𝑝𝑟𝑒𝑑n))). The third clause splits it, the first and second finish each half, and with 𝖨 written for 𝖲𝖪𝖪 the result is

[n]B = 𝖲(𝖲(𝖲(𝖪𝑖𝑠𝑍𝑒𝑟𝑜)𝖨)(𝖪1))⏟the test and the base case(𝖲(𝖲(𝖪𝑚𝑢𝑙𝑡)𝖨)(𝖲(𝖪r)(𝖲(𝖪𝑝𝑟𝑒𝑑)𝖨)))⏟the recursive case,

twenty-six symbols, with r still free in it. Eliminate r from that by the same three clauses and every variable is gone:

F = 𝖲(𝖪(𝖲A))(𝖲(𝖪(𝖲C))(𝖲(𝖲(𝖪𝖲)(𝖲(𝖪𝖪)𝖨))(𝖪E))),

where A = 𝖲(𝖲(𝖪𝑖𝑠𝑍𝑒𝑟𝑜)𝖨)(𝖪1), C = 𝖲(𝖪𝑚𝑢𝑙𝑡)𝖨 and E = 𝖲(𝖪𝑝𝑟𝑒𝑑)𝖨. Thirty-eight symbols, counting 𝖨 as its three. There is not a variable, a parameter, a conditional or a recursive call anywhere in it — only 𝖪, 𝖲, three arithmetic constants and juxtaposition — and 𝑓𝑖𝑥F applied to 4¯ runs, by the two rules of Definition 1.3, to 24.

That is the whole of this part in one object. A program you could have written in your first week is a closed term in two constants (Chapter 1); it has a type, 𝐼𝑛𝑡 →𝐼𝑛𝑡, and the typing that proves so is a proof (Chapter 2); the proof is the one a mathematician would write, in Gentzen’s rules, with the terms rubbed out (§3.4). Every well-typed thing you compile sits on 𝖪 and 𝖲 in exactly this way.

With one exception, and it is the one Chapter 1 warned of. The two constants did not supply the loop: 𝑓𝑖𝑥 had to be added, and Remark 3.22 has just said what adding it costs. Normalisation goes, so 𝑓𝑖𝑥F is a typed term with no normal form; and read as a proof, 𝑓𝑖𝑥 proves everything, ⊥ included. The price of factorial is the consistency of the logic. Every language you use pays it, and pays it knowingly: the loop is worth more to a programmer than consistency is, and a programmer who wants both must say in the type where the recursion is allowed to go — which is a question for a logic with more to say than this one has.

Exercises

Exercise 3.1. Give typings of 𝜆𝑥.𝜆𝑦.x and of 𝜆𝑥.𝜆𝑦.𝜆𝑧.xz(yz) by the rules (3.2), and confirm that the types are those of (1.1) and (1.2). Then run (𝜆𝑥.𝜆𝑦.𝜆𝑧.xz(yz))MNP and count the β steps.

Exercise 3.2. Run (𝜆𝑥.xx)(𝜆𝑥.xx) for three steps. Then run (𝜆𝑥.𝜆𝑦.x)y, first blindly and then renaming the bound y first, and say which answer is the one meant.

Exercise 3.3. Give natural-deduction derivations, in list form, of (A → B → C) → B → A → C and of (A ∧ B → C) → A → B → C. Then write the program each one is.

Exercise 3.4. Find a closed program of type ¬ ⁡¬ ⁡(A ∨¬ ⁡A), and give its derivation.

Exercise 3.5. Find a closed program of type A ∨ B →¬ ⁡(¬ ⁡A ∧¬ ⁡B).

Exercise 3.6. Try to find a closed program of type ¬ ⁡¬ ⁡p → p, for a letter p. Say where every attempt sticks. Keep your attempt; Part 2 returns to this formula.

Exercise 3.7. Theorem 3.10 turns the Hilbert proof of hypothetical syllogism in §2.6 into a derivation; decorated, it is the program 𝖲(𝖪𝖲)𝖪 with 𝖲 and 𝖪 written as λ-terms. That derivation has detours. Run the program to normal form and compare with §3.2.

Exercise 3.8. Prove the “exactly when” of Definition 3.17 in full: show by induction on a typed term in normal form that it is an introduction form with normal-form parts or neutral, giving the cases for 𝐜𝐚𝐬𝐞 and 𝑎𝑏𝑠𝑢𝑟𝑑 carefully, and say where the type is used.

Reading

Girard et al. (1989, chapters 2–4) is the text behind §§3.4–3.6: natural deduction, the identification of proofs with programs, conversions as the removal of detours, and the normalisation theorem proved in full for → and ∧; it is short, free online, and the right next thing to read. Gentzen (1934) is the original, and Prawitz (1965) the book that made normal proofs a subject. Howard (1980) is the correspondence as Howard wrote it down in 1969; Wadler (2015) is the story told to programmers. Next, Chapter 4: for all, and proving from a specification. 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.
Haskell B. Curry and Robert Feys. Combinatory Logic, Volume I. North-Holland, Amsterdam, 1958.
Gerhard Gentzen. Untersuchungen über das logische Schließen I, II. Mathematische Zeitschrift, 39:176–210, 405–431, 1934. Appeared 1934–35; English translation in The Collected Papers of Gerhard Gentzen, ed. M. E. Szabo, North-Holland, 1969.
Jean-Yves Girard, Yves Lafont, and Paul Taylor. Proofs and Types, volume 7 of Cambridge Tracts in Theoretical Computer Science. Cambridge University Press, 1989. Free online.
William A. Howard. The formulae-as-types notion of construction. In J. P. Seldin and J. R. Hindley, editors, To H.B. Curry: Essays on Combinatory Logic, Lambda Calculus and Formalism, pages 479–490. Academic Press, London, 1980. Written in 1969.
Dag Prawitz. Natural Deduction: A Proof-Theoretical Study. Almqvist & Wiksell, Stockholm, 1965.
William W. Tait. Intensional interpretations of functionals of finite type I. Journal of Symbolic Logic, 32(2):198–212, 1967.
Philip Wadler. Propositions as types. Communications of the ACM, 58(12):75–84, 2015.