Type Systems & Type Checking
Type systems are the mathematical discipline of classifying program expressions so that a compiler can prove, before running a single line, that certain classes of runtime errors simply cannot happen.
Overview
Every program manipulates data, and data has shape: a number is not a string, a function is not a boolean, and treating one as the other is a bug. Type systems give us a formal language for describing those shapes — and, crucially, for reasoning about them automatically. At the deepest level, a type is a concise, machine-checkable specification of a value's behavior.
The history of type systems is inseparable from the history of logic. Church's simply-typed lambda calculus (1940) was born not from engineering pragmatism but from the desire to put mathematics on firm foundations. The Curry-Howard correspondence later revealed that types are propositions and programs are proofs — a connection so profound it reshaped both proof theory and programming language design. Modern type systems in Rust, Haskell, TypeScript, and Lean are direct descendants of these ideas.
This module starts with the practical motivation — what goes wrong without types — and then builds the theory precisely: the simply-typed lambda calculus (STLC), its typing rules as logical inference, and the two cornerstone theorems (Progress and Preservation) that together constitute type safety. Along the way you will see how static typing, dynamic typing, and type inference fit into this landscape, and why the slogan "well-typed programs do not go wrong" has a very precise technical meaning.
By the end you will be able to read a typing judgment, construct a type derivation, and understand why your Haskell or Rust compiler rejects a program before it ever runs — and why that rejection is a proof, not a heuristic.
Type systems are one of the most practical applications of mathematical logic in software engineering. A sound static type system turns the compiler into a theorem prover that works on your behalf every time you hit "build." Memory safety bugs (the source of the majority of CVEs in C/C++ codebases), null-pointer exceptions, wrong-argument-order crashes, and entire classes of API misuse are eliminated not by testing but by construction. Languages like Rust use a type-and-borrow system to guarantee memory safety without a garbage collector — a feat that was considered impossible in the 1990s.
Beyond safety, types are a communication medium. A function signature encrypt : Key -> Plaintext -> Ciphertext is a machine-checked contract that tells every caller exactly what goes in and what comes out. In large codebases and long-lived projects, this precision pays compounding dividends: refactoring becomes tractable because the compiler flags every broken assumption. Understanding how type systems are designed and proven correct is therefore essential for anyone who wants to build reliable languages, compilers, or large software systems — and deeply illuminating for anyone who wants to understand why programs behave the way they do.
STLC Type Derivation Tree for (λx:Bool. x) true : Bool
A type derivation tree for the STLC judgment '∅ ⊢ (λx:Bool. x) true : Bool'. The root is the conclusion we want to prove. T-App splits into proving the function has type Bool→Bool and the argument has type Bool. T-Abs reduces to checking the body under an extended context. T-Var is the axiom leaf. Reading bottom-up is how the type checker works; reading top-down is how you construct a proof.
The concepts
01What is a Type?
Informally, a type is a set of values together with the operations that make sense on them. The integer type contains ..., -2, -1, 0, 1, 2, ... and supports addition, subtraction, multiplication. The string type contains "hello", "world", ... and supports concatenation, indexing. Mixing them without conversion is a semantic error.
More precisely, in the tradition of programming language theory, a type is a syntactic classification assigned to expressions. The type system is a set of rules that map every well-formed expression to a type (or flag an error). The assignment is purely syntactic — we read the code, not the runtime values.
This distinction matters: if true then 1 else "hello" has both a 1 (int) and a "hello" (string) at runtime only the 1 is ever evaluated, but a static type system still rejects it because it cannot determine the type of the whole expression without running the program.
Types serve three roles:
- Documentation: a machine-checked annotation of intent.
- Optimization: knowing that a value is always a 32-bit int lets the compiler emit a single machine instruction instead of a dynamic dispatch.
- Safety: the type checker proves that certain bad things (applying a non-function, indexing a non-array) cannot happen at runtime.
-- Haskell: the compiler knows these are different types
add :: Int -> Int -> Int
add x y = x + y
-- add 1 "hello" -- Type error: expected Int, got String
-- This is caught at compile time, not runtime.
02Static vs. Dynamic Typing
The central axis in type system design is when type checking happens.
Static typing checks types at compile time, before the program runs. Every expression is assigned a type by the compiler. Examples: Java, C, Haskell, Rust, TypeScript. The key benefit is that type errors are caught early, and the type annotations serve as lightweight formal specifications.
Dynamic typing defers type checking to runtime. Values carry their type as a tag, and operations check that tag before proceeding. Examples: Python, JavaScript, Ruby, Lisp. The benefit is flexibility — you can put any value in a list, write generic code without explicit type annotations, and prototype quickly. The cost is that type errors surface only when the bad code path is actually exercised (potentially never in testing, then in production).
A common misconception: "dynamically typed" does not mean "untyped." Python values absolutely have types (type(42) is int). The difference is when and by whom types are checked: compiler vs. runtime.
Gradual typing (TypeScript, mypy for Python, typed Racket) is a modern hybrid: you annotate where you want static guarantees, and unannotated parts remain dynamically checked. The type system inserts runtime checks at the boundary between typed and untyped code.
A subtler distinction: strong vs. weak typing. A strongly typed language (Python, Haskell) never silently coerces incompatible types. A weakly typed language (C, JavaScript) may implicitly reinterpret bits (C's void*, JS's 1 + "2" = "12"). Strong/weak and static/dynamic are independent axes.
# Python — dynamic: error only at runtime
def add(x, y):
return x + y
add(1, 2) # fine
add(1, "oops") # TypeError raised at runtime
// TypeScript — static: error at compile time
function add(x: number, y: number): number {
return x + y;
}
add(1, "oops"); // TS2345: Argument of type 'string' is not
// assignable to parameter of type 'number'
03The Simply-Typed Lambda Calculus (STLC)
The lambda calculus is the minimal formal model of computation with functions. Every Turing-complete language can be encoded in it. The simply-typed variant adds a type discipline that rules out nonsensical programs.
Syntax of the STLC:
Types τ ::= Bool | τ₁ → τ₂ (base types and function types)
Terms t ::= x | λx:τ.t | t₁ t₂ | true | false | if t then t else t
x— a variableλx:τ.t— a function (lambda abstraction) that takes an argumentxof typeτand returnstt₁ t₂— function application: applyt₁to argumentt₂
Notice that we annotate the bound variable with its type. This is the key move that makes the calculus "simply typed."
Example terms:
λx:Bool. x— the identity function on booleans, typeBool → Boolλf:Bool→Bool. λx:Bool. f (f x)— appliesftwice, type(Bool→Bool) → Bool → Bool(λx:Bool. if x then false else true) true— negation applied totrue, should reduce tofalse
The STLC is not Turing-complete — every well-typed STLC program terminates. This is a feature for verification, but a limitation for general programming. Real languages add recursion (and thus the possibility of nontermination) on top.
-- STLC terms in Haskell-like pseudocode
-- Identity on Bool
id_bool :: Bool -> Bool
id_bool x = x
-- Apply f twice
apply_twice :: (Bool -> Bool) -> Bool -> Bool
apply_twice f x = f (f x)
-- In pure lambda notation:
-- λx:Bool. x : Bool → Bool
-- λf:Bool→Bool. λx:Bool. f (f x) : (Bool→Bool) → Bool → Bool
04Typing Judgments and Inference Rules
A typing judgment has the form Γ ⊢ t : τ, read "under context Γ, term t has type τ." The typing context Γ is a finite map from variable names to types — it records what type each in-scope variable has.
Typing rules are written as inference rules in the style of natural deduction:
(premises)
───────────────── (rule name)
(conclusion)
The core rules of the STLC:
T-Var (variable lookup):
x : τ ∈ Γ
────────────
Γ ⊢ x : τ
T-Abs (lambda abstraction):
Γ, x:τ₁ ⊢ t : τ₂
─────────────────────────
Γ ⊢ (λx:τ₁. t) : τ₁ → τ₂
T-App (function application):
Γ ⊢ t₁ : τ₁ → τ₂ Γ ⊢ t₂ : τ₁
─────────────────────────────────
Γ ⊢ t₁ t₂ : τ₂
T-True, T-False: Γ ⊢ true : Bool, Γ ⊢ false : Bool
T-If:
Γ ⊢ t₁ : Bool Γ ⊢ t₂ : τ Γ ⊢ t₃ : τ
─────────────────────────────────────────
Γ ⊢ if t₁ then t₂ else t₃ : τ
A type derivation (or type proof) is a tree of these rules whose root is the judgment we want to establish and whose leaves are axioms (T-Var, T-True, T-False). If a derivation exists, the program is well-typed. The type checker's job is to construct (or refute) such a derivation.
The beauty: type checking is syntax-directed — for each syntactic form of term, exactly one rule applies. This makes type checking decidable and efficient (linear in the size of the term for STLC).
-- Derivation for: ∅ ⊢ (λx:Bool. x) true : Bool
--
-- Step 1: T-True gives ∅ ⊢ true : Bool
-- Step 2: T-Var gives {x:Bool} ⊢ x : Bool
-- Step 3: T-Abs gives ∅ ⊢ (λx:Bool. x) : Bool → Bool
-- Step 4: T-App combines steps 1 and 3:
-- ∅ ⊢ (λx:Bool. x) true : Bool ✓
-- In Haskell, GHC constructs exactly this derivation implicitly:
result :: Bool
result = (\x -> x) True
05Type Safety: Progress and Preservation
"Well-typed programs do not go wrong" — this is the informal statement of type safety (also called soundness). But what does "go wrong" mean precisely? In the STLC, it means reaching a stuck term: a term that is not a value but cannot reduce further (e.g., applying true as if it were a function: true false).
Type safety is established by proving two theorems about the STLC's operational semantics (the rules that describe how terms reduce step-by-step).
Theorem 1 — Progress: If ∅ ⊢ t : τ (t is closed and well-typed), then either t is a value, or there exists a term t' such that t → t' (t can take a step).
Reading: a well-typed program is never stuck. It either is already a final answer (a value), or it can make progress.
Theorem 2 — Preservation (also called Subject Reduction): If Γ ⊢ t : τ and t → t', then Γ ⊢ t' : τ.
Reading: evaluation steps preserve types. If you start with a well-typed term, every intermediate term during evaluation has the same type. Types are invariant under reduction.
Together: start with a well-typed term. By Progress, it either is a value or steps. If it steps, by Preservation the result is still well-typed. By Progress again, still not stuck. By induction, the program runs to completion or diverges — but never crashes with a type error.
Both theorems are proved by structural induction on the typing derivation. The key lemma needed for Progress is the Canonical Forms Lemma: if ∅ ⊢ v : Bool and v is a value, then v is true or false; if ∅ ⊢ v : τ₁→τ₂ and v is a value, then v is a lambda abstraction λx:τ₁.t. This tells you that the shape of a value is determined by its type.
-- Preservation example in pseudocode:
-- Term: (λx:Bool. if x then false else true) true
-- Type: Bool (by T-App)
--
-- Step 1 (beta-reduce): if true then false else true
-- Still has type Bool ✓ (Preservation holds)
--
-- Step 2 (if-true): false
-- Still has type Bool ✓
--
-- false is a value → Progress says we're done, and
-- Preservation says the type is still Bool.
-- A STUCK term (not reachable in STLC but imagine untyped LC):
-- true false -- 'true' is not a function, can't apply it
-- The type system rejects this before it ever runs.
06Operational Semantics: Small-Step Reduction
To prove Progress and Preservation, we need to define precisely what it means for a term to reduce. The standard approach is small-step operational semantics (also called structural operational semantics, SOS), which defines a binary relation t → t' ("t reduces to t' in one step").
The reduction rules for STLC:
E-AppAbs (the key beta-reduction rule):
(λx:τ.t₁₂) v₂ → [x ↦ v₂] t₁₂
Applying a lambda to a value substitutes the value for the bound variable.
E-App1 (reduce the function position):
t₁ → t₁'
──────────────────
t₁ t₂ → t₁' t₂
E-App2 (reduce the argument, once function is a value):
t₂ → t₂'
──────────────────
v₁ t₂ → v₁ t₂'
E-IfTrue, E-IfFalse:
if true then t₂ else t₃ → t₂
if false then t₂ else t₃ → t₃
E-If (reduce the condition):
t₁ → t₁'
──────────────────────────────────────
if t₁ then t₂ else t₃ → if t₁' then t₂ else t₃
The call-by-value strategy (used by most languages: ML, Java, Python) is captured by E-App2: we reduce the argument to a value before substituting. Call-by-name (Haskell's lazy evaluation is call-by-need, a memoized variant) would skip E-App2 and substitute the unreduced argument directly.
Substitution [x ↦ v] t (replace free occurrences of x with v in t) must be defined carefully to avoid variable capture — a subtlety that leads to the notion of alpha-equivalence (renaming bound variables).
-- Trace of: (λx:Bool. λy:Bool. x) true false
-- Step 1: E-AppAbs (outermost application)
-- (λx:Bool. λy:Bool. x) true → λy:Bool. true
-- Term: (λy:Bool. true) false
-- Step 2: E-AppAbs
-- (λy:Bool. true) false → true
-- Term: true (a value, done)
-- In Haskell, this is just the 'const' function:
const :: a -> b -> a
const x _ = x
-- const True False => True
07Type Inference
In the STLC as presented, programmers annotate every lambda parameter with its type. Real languages (Haskell, Rust, OCaml, modern C++) infer most types automatically. Type inference is the process of reconstructing type annotations from the program structure.
The foundational algorithm is Algorithm W by Milner (1978), which solves the type inference problem for the Hindley-Milner (HM) type system — an extension of STLC with parametric polymorphism.
The core idea is unification. The algorithm:
- Assign a fresh type variable α to every unannotated subexpression.
- Generate constraints: equations between types. For an application
t₁ t₂wheret₁has typeτ₁andt₂has typeτ₂and the result has typeτ, add the constraintτ₁ = τ₂ → τ. - Unify all constraints: find a substitution (mapping type variables to types) that satisfies all equations simultaneously. If unification fails, there is a type error.
- Apply the resulting substitution to get the inferred types.
Example: λx. x + 1
- Assign type
αtox. +has typeInt → Int → Int, sox + 1requiresα = Int.- Inferred type of the whole term:
Int → Int.
HM type inference is complete: it finds the principal type (most general type) of every typeable expression. Algorithm W runs in nearly-linear time in practice (theoretically almost-linear in the Ackermann function). Rust's type inference is more powerful (it can look ahead to how a value is used) but is consequently incomplete in theory — some programs require explicit annotations.
-- Haskell infers types without any annotations:
twice f x = f (f x)
-- GHC infers: twice :: (a -> a) -> a -> a
-- Why? f is applied to x (so f :: a -> _)
-- f is applied again to the result (so f :: a -> a)
-- thus f :: a -> a, x :: a, result :: a
-- The constraint system for 'twice f x':
-- Let f :: α, x :: β, result of 'f x' :: γ
-- f x => α = β → γ
-- f (f x) => α = γ → δ
-- From both: β → γ = γ → δ => β=γ=δ=a (free variable)
-- Final: twice :: (a->a) -> a -> a ✓
08Polymorphism and the Curry-Howard Correspondence
Parametric polymorphism (generics) lets a single function work over many types. In HM, the type of the identity function is ∀a. a → a — it works for any type a. This is System F (Girard-Reynolds) at its simplest.
The Curry-Howard correspondence is one of the deepest results in logic and computer science: types are propositions, and programs are proofs. Under this correspondence:
| Type theory | Logic |
|---|---|
Type τ | Proposition P |
Term t : τ | Proof of P |
Function type τ₁ → τ₂ | Implication P₁ ⊃ P₂ |
Product type τ₁ × τ₂ | Conjunction P₁ ∧ P₂ |
Sum type τ₁ + τ₂ | Disjunction P₁ ∨ P₂ |
Empty type ⊥ | Falsehood |
Polymorphism ∀a. τ | Universal quantification ∀x. P(x) |
A function f : A → B is a proof that "if A then B." Applying it to a proof a : A gives a proof f a : B. Type-checking is proof-checking. The lambda calculus is a proof calculus.
This is not a metaphor — it is a precise bijection. Proof assistants like Coq, Lean, and Agda are built on this correspondence: writing a program is constructing a proof, and the type checker verifies your proof is valid.
For the practitioner: this means that when you define a type like Result<T, E> in Rust or Either e a in Haskell, you are doing lightweight formal specification. A function f : Input -> Result<Output, Error> is literally a proof sketch: "given a valid input, I either produce a valid output or a well-typed error."
-- Curry-Howard in Haskell:
-- The type (a -> b) -> a -> b is the proposition "(A→B)→A→B"
-- The term \f x -> f x is its proof (modus ponens!)
mp :: (a -> b) -> a -> b
mp f x = f x -- This is modus ponens as a program.
-- The type (a, b) -> a is the proposition "A∧B → A"
fst' :: (a, b) -> a
fst' (x, _) = x -- Proof: from A and B, extract A.
-- The empty type has no inhabitants → corresponds to False
-- A function Void -> a is a proof of "False → anything"
-- (ex falso quodlibet), and it is vacuously true.
09Type Errors, Soundness, and Unsoundness
A type system is sound if every well-typed program satisfies the safety property — Progress + Preservation hold. Soundness is a theorem, not a design goal you can approximate. You either prove it or you don't.
Many real type systems are deliberately unsound for pragmatic reasons:
- Java arrays are covariant:
String[]is a subtype ofObject[], which allows storing a non-String into a String[] array — caught only at runtime withArrayStoreException. - TypeScript with
any: any cast toanyescapes the type system entirely. - C casts:
(int*)ptrcan reinterpret any pointer, bypassing all type guarantees.
These are escape hatches — they grant power at the cost of soundness. The key insight: unsoundness is a choice. Language designers make it deliberately when the alternative (a stricter but sound rule) would be too inconvenient.
Type safety vs. memory safety: a type-safe language can still have memory issues if it uses a garbage collector and GC pressure causes OOM. A memory-safe language additionally guarantees no use-after-free, no buffer overflow, no dangling pointers — Rust's borrow checker provides this through an affine type system.
Blame assignment in gradual type systems: when a runtime check fails at the boundary between typed and untyped code, the system must assign blame correctly — was it the typed side or the untyped side that violated the contract? This is the core technical challenge of gradual typing theory (Tobin-Hochstadt & Felleisen, 2006).
// Java: covariant arrays — a classic soundness hole
String[] strings = new String[1];
Object[] objects = strings; // allowed (covariance)
objects[0] = 42; // compiles! But throws
// ArrayStoreException at runtime
// Haskell: no such hole — type system is sound
-- You cannot put an Int into a [String]; rejected at compile time.
// Rust: sound AND memory-safe via borrow checker
let v: Vec<String> = vec!["hello".to_string()];
// v[0] = 42; // compile error: type mismatch
// Rust also prevents use-after-free, dangling references, etc.
10Subtyping and the Liskov Substitution Principle
Subtyping adds a relation τ₁ <: τ₂ meaning "a value of type τ₁ can be safely used wherever a τ₂ is expected." The subsumption rule allows upcasting:
Γ ⊢ t : τ₁ τ₁ <: τ₂
────────────────────────
Γ ⊢ t : τ₂
The most important subtyping rule for functions is contravariance in the argument, covariance in the return:
τ₁' <: τ₁ τ₂ <: τ₂'
──────────────────────────
(τ₁ → τ₂) <: (τ₁' → τ₂')
Intuitively: a function that accepts a broader input (τ₁' which τ₁ is a subtype of — i.e., accepts more things) and returns a narrower output is strictly more capable — it can do everything the original could do and more.
This formalizes the Liskov Substitution Principle (LSP) from object-oriented design: if S is a subtype of T, then objects of type S may be used wherever objects of type T are expected, without altering the program's correctness.
Variance in generics: List<Cat> is not automatically a subtype of List<Animal> even if Cat <: Animal. If it were, you could add a Dog to a List<Cat> via List<Animal>. This is why Java generics are invariant (unlike arrays). Scala and Kotlin allow explicit variance annotations (+T for covariant, -T for contravariant).
-- Haskell doesn't have subtyping but we can illustrate in Scala:
// Scala variance:
class Animal
class Cat extends Animal
// List is covariant in Scala: List[Cat] <: List[Animal]
val cats: List[Cat] = List(new Cat)
val animals: List[Animal] = cats // OK — covariant
// Function[-A, +B]: contravariant in A, covariant in B
val f: Animal => String = (a: Animal) => a.toString
val g: Cat => String = f // OK: Animal=>String <: Cat=>String
// because Cat <: Animal (contravariant input),
// and String <: String (covariant output)
Key insights
- ✦Types are not just annotations — they are machine-checkable specifications, and a well-typed program is a program with a proof of partial correctness attached.
- ✦The simply-typed lambda calculus is the minimal formal foundation: everything reduces to variables, abstractions, and applications, governed by inference rules that can be verified mechanically.
- ✦Type safety is the conjunction of Progress (no stuckness) and Preservation (types survive evaluation), both proved by structural induction on the typing derivation — not by testing.
- ✦Type inference (Hindley-Milner / Algorithm W) means the programmer need not write types: the compiler reconstructs the most general (principal) type by generating and solving unification constraints.
- ✦The Curry-Howard correspondence reveals that types ARE propositions and programs ARE proofs — function application is modus ponens, pair construction is conjunction introduction, this is not metaphor but mathematical identity.
- ✦Static vs. dynamic typing is about WHEN checking happens; strong vs. weak typing is about whether implicit coercions are permitted — these are independent axes, not a single spectrum.
- ✦Soundness is a theorem that must be proved; many practical type systems (Java arrays, TypeScript 'any', C casts) deliberately introduce unsoundness as an escape hatch, trading safety for expressiveness.
- ✦Variance (covariance, contravariance, invariance) for generic types follows from the function subtyping rule and prevents subtle array-store-style bugs in generic containers.
Common pitfalls
- ⚠Confusing 'statically typed' with 'strongly typed': Python is dynamically but strongly typed (no silent coercions); C is statically but weakly typed (void* casts bypass the type system). These are orthogonal properties.
- ⚠Treating type safety as binary: real systems have escape hatches (casts, 'any', 'unsafe' blocks). Type safety is a property of the language subset you are using, not an all-or-nothing feature of the language.
- ⚠Forgetting that the Preservation theorem is about the TYPE being preserved, not the VALUE: (λx:Bool.x) true reduces to true, and Bool is preserved, but the term itself changes completely.
- ⚠Applying covariance to mutable containers: List<Cat> is NOT safely usable as List<Animal> if the list is mutable, because you could insert a Dog. Covariance is only sound for read-only (producer) positions.
- ⚠Confusing free and bound variables in substitution: [x↦v](λx.x) should NOT substitute inside the lambda because x is rebound there. Naive substitution without tracking binding depth causes variable capture bugs.
- ⚠Assuming type inference always works: HM is complete for STLC+polymorphism, but System F (rank-2+ polymorphism, as used in some Haskell features) type inference is undecidable. Explicit annotations are sometimes required by the math, not just the compiler.
- ⚠Equating 'the type checker accepted my program' with 'my program is correct': types prove *partial* correctness (no type errors), not full correctness. A well-typed program can still have wrong logic, infinite loops, or semantic bugs.
Practice
- 01medium
Construct the full type derivation tree (using the STLC inference rules) for the term: `(λf:Bool→Bool. λx:Bool. f x) (λy:Bool. y) true`. Show every step, labeling each rule used. What is the final type?
- 02hard
Implement a type checker for the STLC (with Bool, Bool→Bool, if-then-else, lambda, application, true, false) in Python or Haskell. Represent terms as an algebraic data type (or class hierarchy). Your type checker should return the type of a term or raise a TypeError with a helpful message. Test it on at least 5 well-typed and 3 ill-typed terms.
- 03medium
Prove the Canonical Forms Lemma for STLC: (a) If ∅ ⊢ v : Bool and v is a value, then v is either 'true' or 'false'. (b) If ∅ ⊢ v : τ₁→τ₂ and v is a value, then v has the form λx:τ₁. t. Write a rigorous proof by cases on the definition of 'value'.
- 04hard
Consider the untyped lambda calculus term `Ω = (λx. x x)(λx. x x)`. (a) Show that Ω reduces to itself (diverges). (b) Try to assign a type to Ω in STLC. What constraint does the self-application `x x` generate, and why is it unsatisfiable? (c) What does this tell you about the relationship between STLC and termination?
- 05medium
Java arrays are covariant: String[] is a subtype of Object[]. Construct a concrete Java program that compiles without warnings but throws an ArrayStoreException at runtime because of this unsoundness. Then explain exactly which STLC subtyping rule Java violates, and why the correct rule (contravariance for mutable containers) would have rejected your program at compile time.
Resources
The definitive textbook. Chapters 8-15 cover everything in this module with full proofs. This is the canonical reference for type theory in PL.
Coq-mechanized proofs of Progress and Preservation for STLC. You prove the theorems yourself interactively. Unmatched for building rock-solid intuition.
Excellent lecture notes covering lambda calculus, type systems, and type safety proofs at the graduate level.
Philip Wadler's beautiful accessible paper on the Curry-Howard correspondence — the history, the mathematics, and the wonder. Essential reading.
MIT OCW course covering type systems, abstract interpretation, and program analysis. Lecture notes are rigorous and freely available.
The original Algorithm W paper. Readable with the TAPL background. Historically essential.
Accessible, fun introduction to types in a real language with full HM inference. Great complement to the theoretical material.
Concrete grounding for how an industrial-strength type system (with affine types / borrow checking) works in practice.
Checkpoints — you can now…
- ✓You can write the typing judgment Γ ⊢ t : τ and explain what each component means, including why the context Γ is necessary.
- ✓You can construct a complete type derivation tree for a given STLC term, labeling each inference rule (T-Var, T-Abs, T-App, T-If, T-True, T-False).
- ✓You can state Progress and Preservation precisely in English and explain why together they constitute type safety — and why each theorem alone is insufficient.
- ✓You can explain what 'stuck' means in the operational semantics and give an example of a stuck term that the type system correctly rejects.
- ✓You can explain the Curry-Howard correspondence with a concrete example: identify a specific type as a proposition and its inhabitant as a proof.
- ✓You can explain why Java array covariance is unsound, construct a counterexample, and describe what the correct variance rule for mutable containers is.
- ✓You can trace the unification steps that Algorithm W performs on a simple polymorphic function like 'twice f x = f (f x)' to infer its principal type.