Appendix B: Typing Rules
These rules give a declarative account of Fun’s source type system. They collect the judgments used throughout the book, including patterns, binding groups, classes, records, and IO. They describe which programs are well typed. The implementation finds those derivations using unification, levels, and constraint solving. Core has its own explicitly polymorphic typing judgment, introduced in Chapter 10.
How to Read a Rule
Read a fraction from bottom to top as a question: to establish the conclusion, establish each premise above the line. Read it from top to bottom as a proof: these premises justify that conclusion. A rule with no premises is an axiom. The name in parentheses identifies the step, not an extra condition (Pierce 2002).
Our main judgment is . It says that expression has type when the variables in have their listed schemes and the class predicates in hold. The semicolon separates environments. It has no meaning in Fun source. For example:
The typing environment is a finite map. Extending it with shadows an outer binding of . A bar means a sequence, so stands for zero or more type variables. Rules with a subscript apply to every member of the indicated sequence. Side conditions written below a rule are part of it.
| Notation | Meaning |
|---|---|
| A monotype: variable, constructor, application, or arrow | |
| A scheme | |
| Sets of class predicates, such as | |
| Term variables and their schemes | |
| Type variables and constructors with their kinds | |
| Capture-avoiding substitution in a type | |
| Free type variables of | |
| The givens entail every predicate in | |
| Monomorphic variables introduced by a pattern |
Types are identified after expansion of fully applied synonyms. Schemes are identified up to renaming of bound variables. The global signature records data constructors, record fields, classes, and instances. All rules below assume that signature has passed the declaration checks.
Kinds and Type Formation
Kinds are and . The star means a type whose expressions can have values. Kind arrows classify type constructors, not term functions.
Every term type and constructor field has kind . In a predicate ,
must have the kind of the class parameter. Thus Functor accepts a
constructor of kind , while Eq accepts a type of kind . Kinds are
inferred jointly within each type-declaration component. Remaining unconstrained
kind variables become . Synonym cycles and partial synonym applications are
errors, even if later unification could otherwise hide them.
Variables, Literals, Functions
Here replaces the quantified variables with monotypes of matching kinds. Constructors use the same rule with their declared schemes. Literals have fixed types, without a class constraint:
A lambda parameter is monomorphic throughout its body. Instantiating id twice
may choose different types. Using a lambda-bound f twice must respect the same
type. Pattern parameters use the pattern rules below before applying (Abs).
Let, Generalization, and Signatures
Here . The generalized variables are free in or , but not in or . Every variable mentioned by must also occur in . The solver first reduces and simplifies , discharges concrete predicates, and resolves every field constraint. Predicates on variables outside this binding’s generalization scope remain in . A residual predicate with no possible generalization or instance is an error. There is no defaulting and no value restriction.
For an unannotated recursive component, assign a fresh monotype to each , then check all bodies under those same assumptions:
Generalize the solved types together relative to the outer environment, using the same constraint and ambiguity conditions as (Let). All recursive right-hand sides must be syntactic functions after desugaring. Recursion within an unannotated component is monomorphic.
An annotation is a promise checked with fresh rigid variables:
Here replaces each quantified variable with a fresh skolem of the same kind. These skolems cannot be unified with arbitrary types or escape into the outer environment. The body’s required predicates must follow from and the instance environment. An annotated binding is available recursively at its specified scheme, which permits polymorphic recursion. Dependency analysis removes edges to annotated bindings before forming unannotated components. Expression annotations are checked by this rule, then instantiated for use as expressions. They do not introduce first-class polymorphic values.
Patterns, Guards, and Cases
The judgment says that a pattern matches a value of type and introduces the variables in .
Here is the empty environment. The pattern environments must have disjoint domains. Literal patterns have the literal’s fixed type and introduce no bindings. A record pattern is its constructor pattern in declaration order, with wildcards for omitted fields. List, tuple, and unit patterns use their ordinary constructors.
Every guarded branch has the case’s result type. A row’s where bindings are
checked by the binding-group rules in the pattern environment, and scope over
both its guards and results. Function equations use the same row judgment after
introducing one argument per pattern column. All equations agree on arity.
Nonexhaustive and unreachable rows produce warnings, not additional typing rules.
Failure to match remains a possible runtime error at any result type.
A nonrecursive pattern binding first checks its right-hand side and pattern at the same type. It then generalizes the introduced binders relative to the outer environment, with the conditions of (Let). The pattern’s binders are not in scope on that binding’s right-hand side.
Nominal Records
Write for the field type in a record declaration parameterized by , and let substitute its actual type arguments. Each rule requires the record constructor to have been identified.
Every declared field occurs exactly once, with no unknown label. The source order determines evaluation order even though field slots use declaration order.
Update labels are distinct and belong to . The same substitution appears in the input, field types, and result, so an update cannot change type parameters.
Inference generates before is known. If only
one record declares , it chooses that record with fresh parameters. Otherwise
it waits for ’s head constructor. The solver repeats this process before
generalization. Any unresolved field is ambiguous (E0245), even if some
whole-program interpretation could choose a record. Field constraints never
enter a scheme. (.l) is checked as \r -> r.l, so the same restriction applies.
Class Entailment and Declarations
The last rule uses a declared instance and a kind-correct substitution that matches its head to the wanted predicate. Entailment of a set requires entailment of every member. Superclass projection and instance application also construct the dictionary evidence used during elaboration.
A class has one parameter. Its method signatures may mention that parameter but may not add constraints of their own. Other method type variables are implicitly quantified. A default method is checked at its declared type with the class predicate given. Superclass relationships must be acyclic.
An instance head is a constructor applied to distinct variables. Its context constrains only those variables. No two instance heads may overlap. Check each method against the class method signature specialized to that head, using the instance context as givens. Required superclass dictionaries must also be obtainable. A missing method requires a default. Deriving generates instances that undergo these same checks.
Data declarations introduce constructor schemes, with their parameters universally quantified and their fields as successive arrow arguments. Constructor result types name the declared datatype. The language has no existential variables, GADT equations, or constructor-specific class contexts.
Sugar, IO, and Whole Programs
These forms have no separate inference mechanism. Their expansions determine their types:
| Surface form | Typing account |
|---|---|
if b then x else y | b : Bool, and both branches have one type |
a && b, a || b | Both operands are Bool, with conditional evaluation |
a + b, other operators | Application of the operator’s scheme |
-e | Application of negate |
(e op), (op e) | A lambda applying the operator to a fresh parameter |
[a..b] | enumFromTo a b, with both bounds Int |
| Lists, tuples, unit | Applications of their data constructors |
where | A local binding group |
For a monad constructor , a bind step requires an action of type and a continuation whose result has type :
An expression statement uses >>, a let statement uses the binding rules,
and a final expression supplies the block’s result. Refutable bind patterns
expand to a case with an error fallback. There is no MonadFail constraint.
IO is abstract to the source checker and has kind . It is an instance
of Monad, but a source program cannot equate IO a with () -> a. That
representation is exposed only during elaboration. Effectful primitives are
visible only while compiling the Prelude.
A complete program kind-checks its declarations, validates its class environment,
checks binding components in dependency order, and solves all remaining
constraints. Its entry point must have type IO (). No unresolved class or field
constraint may survive. Top-level initializers run before that action, in
dependency order. These evaluation requirements complement the typing rules:
well typed does not mean terminating or free of explicit runtime errors.