Lesson 7.8 — Beyond rank 1: higher-rank polymorphism and impredicativity (overview)¶
Techniques: higher-rank polymorphism —
∀to the left of an arrow, checked with annotations by bidirectional subsumption (Odersky & Läufer 1996 [OL96]; Peyton Jones, Vytiniotis, Weirich & Shields 2007 [PJVWS07]; Dunfield & Krishnaswami 2013 [DK13]); impredicativity — instantiating a type variable with a polymorphic type, made practical by GHC's Quick Look (Serrano, Hage, Peyton Jones & Vytiniotis 2020 [SHPJV20]); the limit: type inference for System F is undecidable (Wells 1999 [Wel99]) · Pebble implements: none · Drills: — (see §9) · Prerequisites: Lessons 7.2, 7.4; Lesson 6.4 · Time: 3 hours
Hindley–Milner keeps inference complete by allowing ∀ only at the top of a let-bound type (Lesson 7.2). Two useful programs fall outside: a function that receives a polymorphic function and uses it at two types (both f = (f 1, f True), rank 2), and a data structure that stores polymorphic values ([forall a. a -> a], impredicative). Full System F allows both but its inference problem is undecidable, so every language that supports them asks for annotations and uses them bidirectionally. This lesson is an overview: the definitions, the key algorithmic idea for each, and the results that bound what is possible.
1. Problem and motivation¶
Higher-rank polymorphism¶
The problem. Proposition 7.2.10 showed that fun f -> (f 1, f true) is untypable in HM: a λ-bound variable is monomorphic. Some abstractions need exactly this — runST :: (forall s. ST s a) -> a uses a rank-2 type to keep a state thread from escaping; encodings of existentials and church-encoded data need it too. Odersky and Läufer [OL96] showed that if the programmer annotates the λ's parameter with its polymorphic type, inference for the rest stays HM-like: the annotation is propagated inward by the checking mode of bidirectional typing (Lesson 6.4), and applications are checked by a subsumption relation "is at least as polymorphic as". Peyton Jones et al. [PJVWS07] turned this into GHC's RankNTypes; Dunfield and Krishnaswami [DK13] gave a compact, complete algorithm.
Impredicativity¶
The problem. In predicative systems (HM, RankNTypes) a type variable ranges over monotypes: head :: [a] -> a can be used at a := Int -> Int but not at a := forall b. b -> b. So head ids with ids :: [forall a. a -> a] is rejected, although it is perfectly sound. Allowing it — impredicative instantiation — breaks the guessing that inference relies on: when unifying a with something, the checker cannot know whether a should be a polymorphic type. Years of proposals (MLF, HMF, FPH, Boxy Types) traded predictability for power; GHC 9.2 adopted Quick Look [SHPJV20]: before type-checking the arguments of an application, look at them quickly to find instantiations that are forced to be polymorphic, and otherwise stay predicative.
2. Definitions and algorithms¶
Definition 7.8.1 (Rank; predicative and impredicative instantiation)
The rank of a type: monotypes have rank 0; \(\forall\vec\alpha.\,\tau\) with \(\tau\) of rank 0 has rank 1; \(\sigma_1 \to \sigma_2\) has rank \(\max(\mathrm{rank}(\sigma_1) + 1, \mathrm{rank}(\sigma_2))\) when \(\sigma_1\) is polymorphic (a ∀ to the left of \(k\) arrows raises the rank by \(k\)). A type system is predicative if type variables are instantiated only with monotypes, impredicative if they may be instantiated with polymorphic types. System F is impredicative and of arbitrary rank.
Definition 7.8.2 (Subsumption)
\(\sigma_1 \le \sigma_2\) ("\(\sigma_1\) is at least as polymorphic as \(\sigma_2\)") holds if every instance of \(\sigma_2\) is an instance of \(\sigma_1\); for rank-1 schemes, \(\forall\vec\alpha.\,\tau_1 \le \forall\vec\beta.\,\tau_2\) iff \(\tau_2 = [\vec\tau/\vec\alpha]\tau_1\) for some \(\vec\tau\) (with \(\vec\beta\) treated as rigid). For higher-rank types the relation is contravariant in argument positions and covariant in results (deep skolemization, [PJVWS07, §4.6]; GHC 9 uses the simpler, shallow version).
Higher-rank polymorphism¶
Algorithm 7.8.3 (Checking an application against a higher-rank parameter)
- Input: a function \(f\) whose (annotated) type is \((\forall\vec a.\,\sigma) \to \rho\), an argument \(e\).
- Output: the result type \(\rho\), or a failure at \(e\).
- Precondition: \(f\)'s type is known (a signature); unannotated λ-bound variables are monomorphic, as in HM.
- Postcondition: \(e\) is checked to be at least as polymorphic as \(\forall\vec a.\,\sigma\) (Theorem 7.8.5).
- Invariant: skolems (rigid variables) introduced for \(\vec a\) never escape: no unification variable created outside may be bound to a type mentioning them (the level discipline of Lesson 7.3).
function CheckArgAgainstPoly(e, ∀ā. σ):
ā' ← fresh skolem constants at a new, deeper level # "for an arbitrary a"
Check(e, [ā'/ā]σ) # bidirectional checking mode
if some unification variable of a shallower level was bound to a type with ā':
fail "rigid type variable would escape its scope"
function Check(e, τ) for e = λx. e1 and τ = σ1 → σ2:
bind x : σ1 (σ1 may itself be polymorphic: x can be used at several types)
Check(e1, σ2)
Impredicativity¶
Algorithm 7.8.4 (Quick Look instantiation, simplified from [SHPJV20])
- Input: an application \(h\ e_1 \dots e_n\) whose head \(h\) has a type \(\forall\vec\alpha.\ \tau_1 \to \dots \to \tau_n \to \tau\); the expected result type if known.
- Output: an instantiation of \(\vec\alpha\), possibly with polymorphic types, then ordinary checking of the arguments.
- Precondition: the arguments' guarded types (the type of a variable, of an application headed by a variable with a known type) are available without full checking.
- Postcondition: a variable is instantiated with a polymorphic type only if the application's own arguments or result force it; otherwise instantiation is predicative (Theorem 7.8.7).
- Invariant: Quick Look never binds a variable that ordinary (predicative) inference would bind differently; it only fills variables that inference would otherwise fail on.
function QuickLook(h, args, expected):
instantiate ᾱ with fresh "instantiation variables" κ̄
for each argument e_i with parameter type τ_i[κ̄]:
if e_i is guarded (a variable x, or an application x …) with known type σ_i:
match τ_i[κ̄] against σ_i, binding κ's to (possibly polymorphic) types
if expected is known: match τ[κ̄] against it
replace every κ still unbound by an ordinary monomorphic unification variable
check the arguments against τ_i with the κ's substituted (Algorithm 7.8.3 when polymorphic)
3. Worked examples¶
Higher-rank polymorphism¶
both :: (forall a. a -> a) -> (Int, Bool); both f = (f 1, f True). Checking the body: f : forall a. a -> a is in scope (the annotation), so f 1 instantiates it at Int and f True at Bool. Checking the call both id: Algorithm 7.8.3 skolemizes a to a rigid a' and checks id against a' -> a' — id : forall b. b -> b instantiates b := a': accepted. both not: check not : Bool -> Bool against a' -> a': unify a' = Bool — a' is rigid: rejected ("Couldn't match type a with Bool", §7). Without the annotation, bothInferred f = (f (1 :: Int), f True) is ordinary HM and fails at True.
Impredicativity¶
firstId = head ids with head :: forall b. [b] -> b and ids :: [forall a. a -> a]. Quick Look instantiates b with an instantiation variable \(\kappa\); the argument ids is a variable with known type, so matching [κ] against [forall a. a -> a] binds \(\kappa := \forall a.\,a \to a\) — impredicatively. The result type is then forall a. a -> a, matching the signature. Without ImpredicativeTypes, even the signature ids :: [forall a. a -> a] is rejected (§7).
4. Invariants and correctness¶
Higher-rank polymorphism¶
Theorem 7.8.5 (Predicative higher-rank checking with annotations is sound, complete and decidable)
For the predicative, arbitrary-rank type system of [PJVWS07] (equivalently [DK13]), the bidirectional algorithm terminates and accepts exactly the programs typable with the given annotations, where every λ-bound variable used polymorphically is annotated; HM programs need no annotation.
Proof sketch (full proofs: [DK13, §5–7]; [PJVWS07, §6])
Soundness by induction on the algorithmic derivation, each rule mapping to a declarative rule; the skolem-escape check corresponds to the side condition of \(\forall\)-introduction. Completeness relative to annotated programs: every place where the declarative system guesses a polymorphic type is either annotated (so checking mode supplies it) or is an instantiation, which predicativity restricts to monotypes that unification can find — the same argument as W's completeness (Theorem 7.2.11). Decidability: every rule is syntax-directed on the term or the type, and instantiations are monotypes, so the measure (term size, type size) decreases.
Proposition 7.8.6 (Rank 2 cannot be inferred without annotations by HM's method)
fun f -> (f 1, f true) has the System F types \((\forall a.\,a \to a) \to \mathsf{int} \times \mathsf{bool}\) and \((\forall a.\,a \to \mathsf{int}) \to \mathsf{int} \times \mathsf{int}\), and no most general one among rank-2 types in which the others are instances by HM's instance relation.
Proof
Both types are derivable in System F by annotating \(f\) accordingly. A common generalization would have to assign \(f\) a type from which both \(\forall a.\,a \to a\) and \(\forall a.\,a \to \mathsf{int}\) arise by instantiating free variables of the function's type (the only instantiation HM performs); but these two parameter types differ in whether the result is the argument type or constant, which no substitution of free variables can change inside the bound \(\forall a\). Hence no principal type exists in HM's sense, and an algorithm must be told which one is meant.
Impredicativity¶
Theorem 7.8.7 (Quick Look is sound and conservative; full inference is undecidable)
(a) Every program Quick Look accepts is typable in System F, and every program accepted by GHC's predicative checker is accepted with the same types. (b) Type inference (and even type checking with only λ-annotations missing) for System F is undecidable.
Proof sketch (full proofs: (a) [SHPJV20, §5]; (b) [Wel99])
(a) Quick Look only adds bindings for instantiation variables that the predicative checker would leave as unification variables and then fail on or leave unconstrained, and each binding is justified by an argument's known type, so the elaborated System F term types. (b) Wells reduces the semi-unification problem, proven undecidable by Kfoury, Tiuryn and Urzyczyn, to typability in System F: a term can be built whose typability encodes a given semi-unification instance. Every practical impredicative system therefore restricts where polymorphic instantiations may be guessed — Quick Look to what is visible in one application.
5. Complexity¶
| Technique | Time (worst) | Time (typical) | Space | Variables |
|---|---|---|---|---|
| Higher-rank (predicative, annotated) | as HM (DEXPTIME-complete) plus a subsumption check per argument | near-linear | skolems per polymorphic argument | \(n\) program size |
| Quick Look | one extra pass over each application's arguments: linear in the application | negligible | instantiation variables | \(k\) arguments per application |
| System F inference | undecidable (Theorem 7.8.7b) | — | — | — |
Justification. Subsumption checks are syntax-directed on types; Quick Look inspects each argument's head once. Pathological family: stacking higher-rank arguments does not blow up checking (annotations are given); the blow-ups are HM's (Lesson 7.3 §5), unchanged.
6. Variants and refinements¶
Higher-rank polymorphism¶
- Deep vs shallow subsumption [PJVWS07]: GHC 9 dropped deep skolemization and co/contravariance of arrows in subsumption ("simplify subsumption"), requiring η-expansion in a few programs. Trade-off: simpler, preserves semantics under laziness; some old programs need
\x -> f x. - Polymorphic record fields / first-class modules (OCaml): rank-2 via explicitly polymorphic record fields and methods. Trade-off: no new inference; annotations at the field.
Impredicativity¶
- MLF (Le Botlan & Rémy): types with flexible and rigid bounds give principal types for impredicative programs. Trade-off: complete, but the types are unfamiliar and the implementation complex.
- HMF, FPH, Boxy types: earlier impredicative extensions of GHC-style inference. Trade-off: each was hard to predict; Quick Look replaced them because it is local and simple [SHPJV20].
7. In real compilers¶
Higher-rank polymorphism¶
GHC: RankNTypes; application checking in tcApp (compiler/GHC/Tc/Gen/App.hs) with skolemization in GHC.Tc.Utils.Unify [GHC-App, GHC-Unify]. The user's guide section "Arbitrary-rank polymorphism" describes the rules [GHC-RankN]. OCaml offers rank 2 through polymorphic record fields; Scala 3 through polymorphic function types.
Rank-2 arguments in GHC 9.4: skolems and the unannotated case
Reproduce (GHC 9.4.7):
mkdir -p hs && cat > hs/Rank.hs <<'EOF'
{-# LANGUAGE RankNTypes #-}
module Rank where
both :: (forall a. a -> a) -> (Int, Bool)
both f = (f 1, f True)
ok :: (Int, Bool)
ok = both id
notPoly :: (Int, Bool)
notPoly = both not
bothInferred f = (f (1 :: Int), f True)
EOF
LC_ALL=C ghc -fno-code hs/Rank.hs
Output (the [1 of 1] Compiling line removed):
hs/Rank.hs:11:16: error:
* Couldn't match type `a' with `Bool'
Expected: a -> a
Actual: Bool -> Bool
`a' is a rigid type variable bound by
a type expected by the context:
forall a. a -> a
at hs/Rank.hs:11:16-18
* In the first argument of `both', namely `not'
In the expression: both not
In an equation for `notPoly': notPoly = both not
|
11 | notPoly = both not
| ^^^
hs/Rank.hs:13:35: error:
* Couldn't match expected type `Int' with actual type `Bool'
* In the first argument of `f', namely `True'
In the expression: f True
In the expression: (f (1 :: Int), f True)
|
13 | bothInferred f = (f (1 :: Int), f True)
| ^^^^
What to notice: both's body and both id are accepted (no error on lines 5 and 8). both not fails because a became a rigid skolem (Algorithm 7.8.3). Without the annotation, f is HM-monomorphic and the second use fails — Proposition 7.8.6 in practice.
Impredicativity¶
GHC ≥ 9.2: ImpredicativeTypes enables Quick Look, implemented by quickLookArg in compiler/GHC/Tc/Gen/App.hs [GHC-App]; the user's guide section "Impredicative polymorphism" states the rules [GHC-Impred].
Quick Look in GHC 9.4: a list of polymorphic functions
Reproduce (GHC 9.4.7):
mkdir -p hs && cat > hs/Impred.hs <<'EOF'
{-# LANGUAGE ImpredicativeTypes #-}
module Impred where
ids :: [forall a. a -> a]
ids = [id, \x -> x]
firstId :: forall a. a -> a
firstId = head ids
applyAll :: (Int, Bool)
applyAll = case ids of (f : _) -> (f 1, f True)
EOF
LC_ALL=C ghc -fno-code hs/Impred.hs && echo "Impred.hs: accepted"
sed 's/ImpredicativeTypes/RankNTypes/; s/module Impred/module Pred/' hs/Impred.hs > hs/Pred.hs
LC_ALL=C ghc -fno-code hs/Pred.hs
Output (the [1 of 1] Compiling lines removed):
Impred.hs: accepted
hs/Pred.hs:4:8: error:
* Illegal polymorphic type: forall a. a -> a
* In the type signature: ids :: [forall a. a -> a]
Suggested fix: Perhaps you intended to use ImpredicativeTypes
|
4 | ids :: [forall a. a -> a]
| ^^^^^^^^^^^^^^^^^^
What to notice: with Quick Look, head ids instantiates head's b with the polymorphic forall a. a -> a (§3), and the pattern-bound f is used at two types. The predicative checker refuses even to form the type [forall a. a -> a] — a type variable of the list constructor may not stand for a polymorphic type.
8. Comparison¶
| Technique | Power / precision | Speed (asymptotic · practical) | Output / error quality | Implementation effort | Typical use |
|---|---|---|---|---|---|
| Higher-rank polymorphism | Arbitrary rank with annotations; complete and decidable for predicative instantiation (Theorem 7.8.5); no principal types without annotations (Proposition 7.8.6) | as HM · negligible overhead | Rigid/skolem-escape errors; clear once annotated | Medium: skolems, subsumption, levels | GHC RankNTypes, runST, Scala 3, OCaml record fields |
| Impredicativity | Polymorphic instantiation where one application forces it (Theorem 7.8.7a); full System F undecidable (Theorem 7.8.7b) | linear extra pass · negligible | Fails predictably: only local evidence is used | Medium (Quick Look) to very high (MLF) | GHC ImpredicativeTypes; research languages |
Choose rank-N types with annotations when an API must take polymorphic arguments; keep everything else HM. Choose Quick Look-style impredicativity only when data structures must hold polymorphic values; prefer a wrapper type (newtype Id = Id (forall a. a -> a)) when portability or predictability matters.
9. Assessment¶
| Technique | Quiz ids | Drill | Flashcard tag | Exercises |
|---|---|---|---|---|
| Higher-rank polymorphism | rank-of-type, rank2-check |
— (an overview topic; the quiz computes ranks and checking verdicts) | higher-rank |
— |
| Impredicativity | impredicative-instantiation, system-f-undecidable |
— (as above) | impredicativity |
— |
References¶
See the chapter references.