Lectures onType Theory
Chapter 16
Chapter 16Optional

Modular Type Classes and Implicit Modules

Prerequisites. Direct starred prerequisites: Chapter 11, Chapter 12. No later core chapter depends on this route.

Suppose integer equality is available in two useful forms. The first compares integers literally; the second always returns false. Write their module evidence as EqInt and EqNever. Now consider the surface program module A=using EqInt in struct let f x=eq(x,x) end,module B=using EqNever in struct let y=A.f 3 end. There are two plausible elaborations. If the definition of A.f fixes the instance available at its definition, then A.fλx:Int.EqInt.eq(x,x),B.y=true. If generalization instead exports a class constraint, then A.fΛ(X:EQ).λ(x:X.t).X.eq(x,x), and the call in B may instantiate X with EqNever, giving B.y=false. The two legal elaborations are observably different: the same surface binding has changed from a monomorphic value to an implicitly parameterized one. Local instance scope, polymorphic generalization, and module abstraction cannot be allowed to settle this question independently.

The parenthesized binders above are distinct: X is a module parameter classified by EQ, while x is a term parameter whose type is the selected component X.t.

Explicit module passing avoids the ambiguity. Define same(E:EQ[Int]) x y:=E.eq x y and write same EqInt 3 3. This places an evidence argument at every use. The design problem is to retain explicit modular configuration while reconstructing omitted module evidence predictably.

Exercise 16.1

★★☆ Let EqParity.eq(m,n) hold exactly when m and n have the same parity. Replace EqNever by EqParity, replace the body of f by eq(x,x+2), and let B evaluate A.f 3. Write the two target terms for A.f, reduce the resulting value of B.y in each elaboration, and identify the single source-language decision that must be fixed at the boundary of A.

Classes as signatures and instances as modules

Ground types are finite constructor trees τ::=c(τ1,,τn), where a nullary constructor is written without parentheses. Thus Int, Bool, List(Int), and Pair(Int,Bool) are ground types. Their size is |c(τ1,,τn)|:=1+i=1n|τi|.

Definition 16.1 — Atomic class signature

An atomic class signature has one distinguished type component t and finitely many value components whose types may mention t. Its realization at τ, written K[τ], makes the equation t=τ transparent. The running signatures are EQ:=sig {type t;val eq:ttBool},SHOW:=sig {type t;val show:tString}. A module V:K[τ] is evidence that the operations of K are available at τ.

Evidence for K[τ] is a module V with a transparent component equation V.t=τ. Resolution therefore returns module paths and applications such as EqListEqInt; uniqueness means syntactic equality of those evidence trees, not observational dictionary coherence.

For example, EqInt:EQ[Int],EqBool:EQ[Bool],ShowInt:SHOW[Int],ShowBool:SHOW[Bool]. The type component matters. A module containing an integer comparison but claiming t=Bool is not approximately suitable evidence; it is ill typed.

Compound evidence is constructed by module functors. The equality functors for products and lists have the interfaces EqPair:EQ[α],EQ[β]modEQ[Pair(α,β)],EqList:EQ[α]modEQ[List(α)]. Here mod describes a module functor interface, not an object language implication. Applying these functors gives the evidence term EqListEqPairEqInt,EqBool:EQ[List(Pair(Int,Bool))]. An evidence functor is total and pure. Hence resolving FV1,,Vn neither diverges nor creates a fresh abstract type; repeated applications to the same transparent evidence denote the same evidence expression.

The corresponding display functors have interfaces ShowPair:SHOW[α],SHOW[β]modSHOW[Pair(α,β)],ShowList:SHOW[α]modSHOW[List(α)].

Definition 16.2 — The finite calculus MTC_0

An instance declaration for a class K and constructor c of arity n has the form F:K1[αj1],,Km[αjm]modK[c(α1,,αn)], where every jr lies in {1,,n}. Its application to evidence V1,,Vm is written FV1,,Vm. A nullary declaration is simply a named module path P:K[c].

A finite availability environment Θ is admissible when:

  1. every declaration is well typed at its displayed interface;

  2. each premise requests evidence only for an immediate argument of the result constructor; and

  3. for every pair (K,c), Θ contains at most one declaration with result head K[c()].

The second condition is the decreasing condition; the third is nominal non-overlap.

The decreasing condition rejects EqAgain:EQ[α]modEQ[α]. Its result has no constructor layer that is removed by the premise. The module may be extensionally harmless, but unrestricted search can apply it forever. The condition also excludes useful instances whose premises are not structural subcomponents. It is deliberately stronger than the full calculus; a small theorem with visible hypotheses is preferable to a broader algorithm whose termination argument is left implicit.

Exercise 16.2

★☆☆ Using the displayed ShowPair and ShowList functors, construct the complete evidence module for SHOW[List(Pair(Bool,Int))], and state the type of every proper evidence subterm.

Availability is not declaration

A repository may contain both EqInt and EqNever. They conflict only if both are adopted for the same inference scope. We therefore distinguish a repository R of named, separately checkable modules from the finite lexical subenvironment ΘR used by resolution.

Definition 16.3 — Explicit adoption

An adoption is the lexical act of adding one named module from the repository to the finite subenvironment used for resolution. The top-level phrase using P in T checks that PR, that its interface is an admissible instance declaration, and that adding it preserves non-overlap. It then checks the top-level module phrase T under Θ,P. The phrase does not add P globally, and P need not be canonical in any sibling module.

The distinction gives three separate operations: operationeffectdeclare PP becomes a named module in Radopt PP becomes available in the selected Θresolve K[τ]construct evidence from the lexically selected Θ. Conflating the first two operations recreates the global-instance problem. Conflating the second and third hides configuration inside search.

The opening counterexample also shows why adoption cannot be inserted under an arbitrary expression and then forgotten at generalization. Let mInner(S) mean that m is an ordinary term or reduced-module phrase of chapter 12, checked at signature S, with no using form. The outer boundary grammar is exactly T::=m:>Susing P in T(mInner(S)). Only T admits using. The ascription m:>S fixes the signature exported across the boundary. In the opening program, the signature of A must therefore choose between val f:IntBoolandval f:X:EQ.X.tBool. The first captures EqInt; the second advertises a module parameter and lets each caller supply it. Scope no longer guesses which interface the programmer intended.

The finite elaboration judgment begins after the exported signature has chosen one of these two types for A.f. It inserts evidence for an already explicit class request; it does not decide whether A.f is generalized.

Lemma 16.4 — Compatible extension

Let Θ be admissible, and let D be a well-typed declaration satisfying the decreasing condition whose result head (K,c) does not occur in Θ. Then Θ,D is admissible. Every declaration already in Θ retains its interface.

Proof of Lemma 16.4 — Compatible extension

Proof. Well-typedness and the decreasing condition hold for old declarations by the admissibility of Θ, and for D by hypothesis. The only possible new overlap would involve D. Its result head is absent from Θ, so no such pair exists. Extending an availability environment does not alter the signatures of its existing paths or functors. ◻

Exercise 16.3

★★☆ Let Θ contain EqInt and EqList. For each of the following proposed adoptions, say whether lemma 16.4 applies and give the exact reason: ShowInt:SHOW[Int],EqNever:EQ[Int],ShowList:SHOW[α]modSHOW[List(α)]. For the rejected case, exhibit two distinct evidence terms for the same request if non-overlap were ignored.

Head-directed resolution

Resolution should construct the omitted module expression and nothing else. The judgment ΘK[τ]resV means that the available declarations construct evidence V for the realized signature K[τ]. It is generated by two rules.

P:K[c]Θ
ΘK[c]resP
R-Base
F:K1[αj1],,Km[αjm]modK[c(α1,,αn)]ΘΘKr[τjr]resVr(1rm)
ΘK[c(τ1,,τn)]resFV1,,Vm
R-Functor

Evidence typing is the least judgment generated by the corresponding two declaration rules:

P:K[c]Θ
ΘP:K[c]
T-EvPath
F:K1[αj1],,Km[αjm]modK[c(α1,,αn)]ΘΘVr:Kr[τjr](1rm)
ΘFV1,,Vm:K[c(τ1,,τn)]
T-EvFunctor

These are the evidence judgments used in the resolver theorem; field typing is added only when the evidence is inserted into ordinary terms.

Resolve the pair request first and name its evidence Vpair:=EqPairEqInt,EqBool. The pair evidence is derived by EqIntΘΘEQ[Int]resEqIntRBaseEqBoolΘΘEQ[Bool]resEqBoolRBaseΘEQ[Pair(Int,Bool)]resVpairRFunctor. The outer application then has one premise: ΘEQ[Pair(Int,Bool)]resVpairΘEQ[List(Pair(Int,Bool))]resEqListVpairRFunctor.

A naive resolver might enumerate all module expressions and test their signatures. That search rediscovers irrelevant functors, repeats identical subproblems, and diverges in the presence of EqAgain. In MTC0, the result type already determines the only declaration that could finish a derivation. Resolution can therefore follow the pair (K,c) at the root.

Definition 16.5 — Deterministic resolver

For admissible Θ, the resolver is the diagnostic function resolveΘ(K,τ) recursively on |τ|. Its result is either evidence V or a diagnostic missing(K,τ); the induced evidence-only selector is partial. For τ=c(τ1,,τn), look up the unique declaration with result head (K,c).

  1. If no declaration exists, return missing(K,τ).

  2. If it is a nullary path P, return P.

  3. If it is a functor F with premises Kr[αjr], recursively resolve Kr[τjr]. Return FV1,,Vm when all calls succeed, and propagate the first missing request otherwise.

An overlap is rejected when Θ is validated, before this function is called.

resolveΘ returns transparent module syntax V:K[τ]; dictionary entailment in chapter 11 instead returns an ordinary target term.

Theorem 16.6 — Exactness and uniqueness of MTC_0 resolution

Let Θ be finite and admissible.

  1. resolveΘ(K,τ) terminates.

  2. If it returns V, then ΘK[τ]resV and ΘV:K[τ].

  3. If ΘK[τ]resV, then the resolver returns V.

  4. If both ΘK[τ]resV and ΘK[τ]resW, then V=W.

Equality in the last clause is syntactic equality of evidence trees, modulo alpha-renaming inside the named modules.

Proof of Theorem 16.6 — Exactness and uniqueness of MTC_0 resolution

Proof. For termination, recurse on |τ|. Every functor premise requests an immediate argument τjr, and |τjr|<|c(τ1,,τn)|. A finite number of smaller calls therefore terminates.

For soundness, use the same induction. A returned nullary path gives R-Base. In the functor case, each recursive call gives ΘKr[τjr]resVr. Applying R-Functor gives ΘK[c(τ¯)]resFV1,,Vm, and ordinary functor application typing gives ΘFV1,,Vm:K[c(τ1,,τn)].

For completeness, induct on the final rule of the assumed resolution derivation. In rule R-Base, the final premise selects a declaration with head (K,c); admissibility says that no second selected declaration has that head, so lookup returns this declaration uniquely. In rule R-Functor, the same non-overlap condition proves that F is the only possible root declaration. Each premise has the unique evidence returned by the induction hypothesis, so every derivation of the request has the same outer constructor and subtrees.

For uniqueness, apply completeness to both derivations. A deterministic function cannot return two different results for one input. ◻

Corollary 16.7 — Resolution is stable under admissible extension

Let ΘΘ be finite admissible environments. Then resolveΘ(K,τ)=VresolveΘ(K,τ)=V.

Proof of Corollary 16.7 — Resolution is stable under admissible extension

Proof. Induct on the evidence tree V. At each node the declaration used in Θ remains present in Θ. Admissibility of Θ prevents any second declaration at that result head. The recursive premise lookups are unchanged by the induction hypotheses. ◻

Exercise 16.4

★☆☆ Let Θ contain EqInt, EqPair, and EqList, but not EqBool. Trace resolveΘ on EQ[List(Pair(Int,Bool))]. Give the sequence of lookup keys in depth-first left-to-right order, the first reported missing request, and the derivation rule whose premise cannot be completed.

Elaboration inserts module evidence

To elaborate eq[τ](e1,e2), resolve EQ[τ]resV, elaborate both arguments at τ, and return V.eqe1e2. Consider the ground source fragment with explicit type indices: e::=xλx:τ.ee eeq[τ](e,e)show[τ](e).

The target’s ordinary fragment is the simply typed lambda calculus on these ground types. Its module-evidence expressions are V::=PFV1,,Vm,e::=xcλx:τ.eeeV.f, where c ranges over the ground literals and primitives. The environment Θ binds every path and total functor at its realized interface. Write K¯[α¯] for K1[αj1],,Km[αjm]. Besides the ordinary lambda rules, target typing has P:K[c]ΘΓ;ΘP:K[c]ModPathΘ(F)=K¯[α¯]modK[c(α¯)]Γ;ΘVr:Kr[τjr](1rm)Γ;ΘFV1,,Vm:K[c(τ¯)]ModFunctor. and Γ;ΘV:K[τ]f:A(t) is a field of KΓ;ΘV.f:A(τ)ModField, Thus FV¯ has its declared realized result when every Vi has the corresponding realized premise interface. These rules type the finite slice’s module evidence.

Lemma 16.8 — Evidence embedding into the term context

If ΘV:K[τ], then Γ;ΘV:K[τ] for every well-formed term context Γ.

Proof of Lemma 16.8 — Evidence embedding into the term context

Proof. Induct on the evidence-typing derivation. An application of T-EvPath becomes Mod-Path. In the T-EvFunctor case, apply the induction hypotheses to every evidence argument and finish with Mod-Functor. These are the two evidence forms. ◻

Ordinary forms elaborate homomorphically. The two interesting rules are

ΘEQ[τ]resVΓ;Θei:τei(i=1,2)
Γ;Θeq[τ](e1,e2):BoolV.eq e1 e2
E-Eq
ΘSHOW[τ]resVΓ;Θe:τe
Γ;Θshow[τ](e):StringV.show e
E-Show

For example, let τ0:=List(Pair(Int,Bool)),V0:=EqListEqPairEqInt,EqBool. The premise ΘEQ[τ0]resV0 and two variable rules give x:τ0,y:τ0;Θeq[τ0](x,y):BoolV0.eq x y. The target records the module expression selected by resolution.

Proposition 16.9 — Type preservation of evidence insertion

Assume Θ is admissible and every module field projection obeys its realized signature. If Γ;Θe:τe, then Γ;Θe:τ in the explicit module target, where the declarations in Θ are bound at their displayed module interfaces.

Proof of Proposition 16.9 — Type preservation of evidence insertion

Proof. Induct on the elaboration derivation. Variables, abstractions, and applications are the corresponding target typing rules. In E-Eq, theorem 16.6 gives ΘV:EQ[τ]. By lemma 16.8, the same evidence is typed under Γ;Θ, and signature projection gives Γ;ΘV.eq:ττBool. The induction hypotheses type e1 and e2 at τ; two applications give the conclusion. The E-Show case is identical with field type τString. ◻

The proposition is preservation, not a theorem that every source program has an elaboration. Missing evidence, rejected overlap, and an unannotated ambiguous type can all prevent a derivation.

Exercise 16.5

★★☆ Assume the SHOW base modules and functors displayed in section 16.1. Elaborate λz:List(Pair(Bool,Int)).show[List(Pair(Bool,Int))](z). Then give a target typing derivation for the inserted field projection and state exactly where admissibility is used.

Residual constraints and polymorphic evidence

Ground resolution constructs a closed module expression. Type inference must also handle requests containing unknown types. Consider λx:α.eq[Pair(Int,α)]((0,x),(0,x)). The outer constructor already determines EqPair. The integer premise is solved by EqInt, while the request EQ[α] remains. Introduce an evidence variable X:EQ[α]. The partially constructed evidence is EqPairEqInt,X, and the generalized target is Λα.λX:EQ[α].λx:α.(EqPairEqInt,X).eq((0,x),(0,x)). The residual constraint is not a failure. It is the interface exported by generalization.

Definition 16.10 — Constraint reduction in the finite slice

A symbolic request is K[ρ], where ρ may contain type variables. Normalize type variables to de Bruijn indices and fix a total order on requests: first the fixed class-name order, then the lexicographic order on normalized type syntax. Reduction maintains a memo table from requests to evidence expressions.

  1. If K[ρ] is already in the table, reuse its recorded evidence.

  2. If ρ is a bare type variable, record one residual hole for the request and return that hole.

  3. If ρ=c(ρ¯), follow the unique declaration at head (K,c), reduce its premises from left to right, and record the resulting path or functor application. A missing head is reported as in ground resolution.

After the root call, retain the distinct residual requests, sort them by the fixed order, name them X1,,Xq, and replace every memoized hole by the corresponding variable. Thus repeated requests share one parameter, and the exported parameter order is independent of traversal accidents. The output is a pair (Σ,V), where Σ=X1:K1[α1],,Xq:Kq[αq] is the residual evidence context and V is an evidence expression well typed under Θ,Σ.

For example, reducing EQ[Pair(α,α)] produces (X:EQ[α],EqPairX,X), not two independently generalized parameters.

Here Σ denotes the residual module-evidence context; in lemma 16.11, lowercase σ denotes an evidence substitution.

For the request EQ[Pair(Int,α)], reduction returns (X:EQ[α],EqPairEqInt,X). Substituting any ground evidence W:EQ[τ] for X yields well-typed evidence at Pair(Int,τ). It yields exactly the evidence obtained by ground resolution when W is itself the evidence selected by the ground resolver for EQ[τ].

Lemma 16.11 — Grounding residual evidence

Let Θ be admissible, and suppose constraint reduction returns (Σ,V) for K[ρ]. Let η replace every type variable in ρ by a ground type. Let σ map each assumption X:KX[αX] in Σ to evidence satisfying ΘσX:KX[ηαX]. Then ΘσV:K[ηρ]. If, in addition, resolveΘ(KX,ηαX)=σXfor every X:KX[αX]Σ, then resolveΘ(K,ηρ)=σV.

Proof of Lemma 16.11 — Grounding residual evidence

Proof. Induct on the number of memo-table insertions, with a subsidiary induction on the constructor depth of a newly inserted request. A memo hit performs no insertion: it reuses evidence already typed by the induction hypothesis, and σ acts on that shared expression only once. A retained variable request is typed after substitution by the first hypothesis on σ. Under the additional hypothesis, it is also resolved to σX. A solved nullary request is typed and resolved by its declaration. For a functor node, apply the induction hypotheses to the premise evidence and then the functor’s declared interface. After grounding, the resolver follows the same unique head declaration and, by the induction hypotheses, returns the substituted premise subtrees. It therefore returns σV. In the final renaming pass, every occurrence of one memoized residual hole is replaced by the same Xi. Renaming the finite residual telescope preserves typing, so sorting and coalescing cannot duplicate an assumption or change the evidence type. ◻

Exercise 16.6

★☆☆ Reduce SHOW[List(Pair(α,List(Int)))] under ShowInt, ShowPair, and ShowList. Give the residual context, the evidence expression, and its result after grounding α to Bool with evidence ShowBool.

The published modular-type-class calculus

Definition 16.12 — System card for modular type classes

The source is the extended technical presentation dated 26 October 2006 and the POPL 2007 publication. Its external language elaborates into an explicitly typed higher-order module language. It adds the following load-bearing forms to ordinary terms, signatures, modules, and top-level modules: sig::=canon(sig),mod::=overload  from sigimplicit(P)explicit(P:S),top::=m:>Susing P in top. The internal language distinguishes ordinary functors from total functors; canonical instance construction may apply only the latter. The set Θ contains paths explicitly adopted as canonical. Declarative elaboration uses judgments for terms, modules, top-level modules, coercive signature matching, class decomposition, usability, and canonical module construction. Type inference extends Algorithm W with substitutions and residual module constraints.

Three forms deserve immediate calculation.

First, canon(S) requests the canonical module at a concrete class signature S. Its class parameters must be transparent, but associated type components may remain abstract. Thus a class can expose the selected carrier while retaining additional type-level information supplied by the instance.

Second, overload eq from EQ elaborates to a constrained polymorphic value whose module parameter supplies the projection X.eq. Instantiating that value finds a canonical module of a transparent realization of EQ and inserts it as a total-functor argument.

Third, using P in T first checks that P is usable, including the source calculus’s structural non-overlap test, and only then checks T under Θ,P. The form exists only at the top-module stratum, which is the formal repair for the scope/generalization failure at the beginning of the chapter.

Canonical evidence is not restricted to a named base path. The selected rules admit:

  1. singleton modules for transparent type components;

  2. componentwise canonical structures;

  3. a path PΘ at its atomic class signature;

  4. application of a canonical total functor from Θ to canonical argument evidence; and

  5. conversion along equivalent signatures.

The fourth clause is the full-calculus analogue of R-Functor.

The inference algorithm does not immediately guess closed evidence for every constraint. It produces a type-and-module substitution δ and a constraint context Σ. Constraint normalization performs canonical backchaining. For example, an equality constraint at Pair(Int,α) reduces to a residual equality constraint at α, with evidence EqPairEqInt,X. This is the higher-order, structural-signature version of definition 16.10.

Convention 16.13 — Imported inference interface

An arrow records an algorithmic output; its decorated form additionally says that residual constraints have been normalized. A suffix /(Σ;δ) contains the residual module constraints and inferred substitution. The relations , cn, and cn are, respectively, coercive signature matching and the two constraint-processing phases. The arrow is the source’s declarative elaboration separator. Thus the declarative expression and module judgments have signatures Θ;Γexpe:τ,Θ;ΓmodM:S. Here exp and mod are source expressions and modules, whereas e and M are their intermediate-language elaborations.

The soundness hypotheses use the following definitions from Figure 22. Write Ks for the atomic signature containing one kind component K, and τs for the atomic signature containing one type component at τ; this signature embedding is not semantic denotation. An intermediate-language object is ground when it has no free unification variables. A kind is legal when it is the image of an external-language kind. A signature is legal when every embedded signature Ks contains a legal kind, every result of a dependent functor ΠX:S1.S2 is a structure signature, and every result of X:S1.S2 is either a structure signature or τs; in the latter case ΓS1class must hold in the context where S1 occurs. A context is legal when each kind and signature declared in it is legal. The imported judgments maintain legality as an invariant.

A signature S is synthesis when every signature occurring within it is ground, except that a sub-signature τs or X:R.τs may be non-ground when it does not occur in the argument of a functor signature. A context Γ is synthesis exactly when every declaration α:K has ground K and every declaration X:S has synthesis S. The context (Θ;Γ) is valid for inference exactly when (Θ;Γ)ok,Γ is synthesis,PΘsigΓ(P) is ground. Here sigΓ(P) is the signature assigned to P by Γ. The premise (Θ;Γ)ok abbreviates the source’s two context-formation relations: XokΓokΓKkindΓ,α:Kok,ΓokΓτ:TΓ,x:τokΓokΓSsigΓ,X:Sok. and Γok(;Γ)ok(Θ;Γ)okΘ;ΓPusable(Θ,P;Γ)ok.

Let Γ0 and Γ1 be contexts, and let θ be a type substitution. Define the context-substitution judgment by Γ0θ:Γ1Γ0okandΓ0θΓ1,αdom(θ).Γ0θα:T. For a constraint context Σ, define simultaneous canonical evidence by Θ;Γcanσ:Σ(X:S)Σ.Θ;ΓcanσX:S.

The selected interface has the following judgment signatures: Θ;Γexpe:τ/(Σ;δ),Θ;Γexpe:τ/(Σ;δ),Θ;ΓmodM:S/(Σ;δ),Θ;ΓmodM:S/(Σ;δ),Θ;ΓΣ0cn(Σ;σ;δ),Θ;ΓΣ0cn(Σ;σ;δ),Θ;Γcanσ:Σ,Γ0θ:Γ1,Θ;ΓtopM:S,Θ;ΓtopM:S. These are the expression, module, constraint-processing, substitution, canonical-evidence, and top-level fragments of Figures 22–23 in the extended presentation [DHCK07]. No rule outside that named source system is implicit in the notation.

Theorem 16.14 — Imported: published inference soundness

Let the algorithmic judgments be those of convention 16.13. If (Θ;Γ) is valid for inference, then each selected expression, module, or constraint-processing judgment with input Θ;Γ and output substitution δ satisfies δΓδ:Γ. Suppose further that ΘΘ, Γδ:δΓ, and (Θ;Γ)ok. Suppose also that Θ;Γcanσ:δΣ. Then the four projections used in this chapter hold:

  1. If either Θ;Γexpe:τ/(Σ;δ) or Θ;Γexpe:τ/(Σ;δ), then Θ;Γexpσδe:δτ.

  2. If either Θ;ΓmodM:S/(Σ;δ) or Θ;ΓmodM:S/(Σ;δ), then Θ;ΓmodσδM:δS.

  3. If constraint normalization returns Θ;ΓΣ0cn(Σ;σ;δ) or Θ;ΓΣ0cn(Σ;σ;δ), then Θ;Γcanσδσ:δδΣ0.

  4. If (Θ;Γ)ok, Γ is ground, and Θ;ΓtopM:S, then Θ;ΓtopM:S, and S is ground.

Proof of Theorem 16.14 — Imported: published inference soundness

Imported proof. Source. The soundness theorem in [DHCK07] supplies clauses 5, 7, 9, and the final top-level clause of its mutual statement: these are respectively the expression, module, constraint-processing, and top-level projections used here. The preceding convention fixes the signature used here. Solving residual constraints by σ gives the displayed judgments in the compatible extension Θ;Γ. No completeness, coherence, or result for MTC0 is imported. ◻

The expression show(read("1")) can leave the intermediate carrier unconstrained. Distinct reader/printer module expressions can then solve the constraints, so the algorithm rejects the ambiguity. Soundness types one returned elaboration; coherence would compare all returned elaborations.

Exercise 16.7

★☆☆ For each claim below, state whether it follows from theorem 16.14; if not, name the missing kind of result.

  1. Every accepted top-level module has a declarative elaboration.

  2. Every declaratively typable module is accepted by inference.

  3. Two successful elaborations of the same source program are observationally equivalent.

  4. Solving the residual constraints in a compatible future world preserves the inferred term’s declarative typing.

Modular implicits: evidence is a module expression

The examples in this section follow OCaml surface notation: list is postfix and a module functor is applied with parentheses. The formal MTC0 calculus above retains prefix List(A) and angle brackets FV; the notation change does not identify the two resolution systems.

Modular implicits move the implicit parameter into ordinary function syntax. For a module type S, a function may have an implicit module parameter {M:S}τ. An explicit call writes f{M}x; an omitted argument asks the resolver to construct a module expression. Candidate modules enter the search space through implicit module, local implicit-module bindings, implicit parameters, and explicitly opened implicit namespaces.

With show {S:SHOW} (x:S.t)=S.show x,ShowList {S:SHOW}:SHOWwith t=S.t list, the call show [1,2] constrains the missing module M by M.t=Int list. Trying the functor ShowList transforms that constraint into S.t=Int, which ShowInt solves. The evidence is the module expression ShowList(ShowInt), not merely the name of a global dictionary.

Let I be the finite, lexically ordered collection of implicit module paths and total implicit-module functors currently in scope. Write I;ΓScand{V1,,Vn} for candidate search after inference has generated the type-component equations for an omitted parameter of module type S. A module path is a candidate when its signature matches S. A functor application F(V1,,Vn) is a candidate when its result matches S and each strictly earlier argument request has the unique candidate Vi. Reject cycles and nondecreasing self-applications, and quotient the resulting finite set by alpha-equivalence. The omitted-call boundary is I;ΓScand{V}Γf:{M:S}τ1τ2Γx:τ1I;Γf xf{V}x:τ2MICall. Zero candidates is a missing-implicit error; two distinct candidates is an ambiguity error. For the list example the carrier equation first admits ShowList(S), reduces its premise to S.t=Int, and closes uniquely with S=ShowInt. If both ShowList and CompactShowList are eligible, the same calculation produces two candidates and MI-Call is inapplicable.

Definition 16.15 — Resolution discipline of modular implicits

The published design resolves one omitted implicit module in three stages.

  1. Type inference gathers equations constraining the missing module’s type components from explicit arguments and the expected result.

  2. Search constructs module expressions from unqualified implicit modules and implicit functors in scope, using module-type inclusion and the gathered equations to test candidates.

  3. The call is accepted only when the resulting module expression is unique up to the implementation’s alias-equivalence test. Search must also satisfy the specified decreasing check for repeated applications of the same implicit functor.

The decreasing check compares the constraints passed to successive applications of one functor. Thus ShowList(ShowList(ShowInt)) is allowed when resolving an integer-list-list request: the carrier constraint loses one list constructor at each repeated application. In contrast, ShowAgain(ShowAgain()) with ShowAgain(S).t=S.t does not decrease and is rejected. Termination is required before uniqueness can be checked; silently ignoring a nonterminating branch could miss a second candidate.

Uniqueness concerns complete module expressions. If the two candidates are V1=ShowList(ShowInt),V2=CompactShowList(ShowInt), then the call is ambiguous even if both modules print some legal string. If two inheritance paths construct extensionally similar modules but are not recognized aliases, they also remain distinct candidates. Resolution is therefore not ordered by a hidden preference rule.

Resolving SHOW for a list of pairs constructs the nested evidence Vpair=ShowPair(ShowString,ShowInt),Vlist=ShowList(Vpair).

Prototype specimen.

The archived modular-implicits checkout recorded in appendix E contains the regression example testsuite/tests/typing-modular_implicits/show.ml, whose observations are

"4"
5
[("hello",1); ("world",2)]
5.5

Appendix E records the checkout identity separately from the Kappa companion.

The archived prototype output confirms that this module expression elaborates the sample list [WBY15].

Exercise 16.8

★☆☆ Assume the following implicit modules are available: ShowInt,ShowList,CompactShowList. The two functors have the same result carrier S.t list but different printing operations. Resolve show [1,2] as far as possible. List every candidate module expression, state the common generated constraint, and explain why the decreasing check succeeds while uniqueness fails.

Implicit function types are lexically scoped functions

An implicit function need not denote a canonical class instance or a module. Consider the source term let ?:Int=1 in let f:Int?Int=? in let ?:Int=2 in f. The body used to define f is checked under a fresh implicit integer parameter, so it elaborates to the identity function. At the final occurrence of f, automatic implicit application chooses the innermost integer, namely 2. The result is 2. There is no globally canonical Int evidence, no distinguished type component, and no module expression to synthesize.

SI has ordinary arrows, implicit arrows, polymorphism, explicit variables, and query ?; both arrows elaborate to ordinary System F arrows [OBL^+18]. Restricted types and full types are R::=bXTT,T::=RT?TX.T,b::=Int. Terms include explicit variables and functions, the query ?, ordinary and implicit let bindings, type abstraction/application, and a stitching annotation. Bidirectional typing simultaneously elaborates into System F. Integer literals and addition elaborate homomorphically and never invoke implicit search.

The source uses one ordered context Γ, writing explicit bindings as x:T and implicit bindings as ?y:T. The tags are part of the context syntax: x ranges over explicit variables and y over implicit variables, so the two membership tests below are disjoint. The judgments ΓeTu and ΓeTu respectively synthesize and check while producing a System F term. The type translation () maps both ordinary and implicit arrows to ordinary arrows and is homomorphic elsewhere. The typing and elaboration rules are:

x:TΓ
ΓxTx
SI-Var
?y:TΓ
Γ?Ty
SI-Query
Γ,x:SeTu
Γλx.eSTλx:S.u
SI-ArrI
Γe1STuΓe2Su
Γe1e2Tuu
SI-ArrE
y freshΓ,?y:SeTu
ΓeS?Tλy:S.u
SI-ImpI
ΓeS?TuΓ?Su
ΓeTuu
SI-ImpE
Γ,XeTu
ΓeX.TΛX.u
SI-AllI
ΓeX.Tuftv(S)domty(Γ)
Γe[X:=S]Tu[S]
SI-AllE
Γe1TuΓ,x:Te2Ru
Γlet x:T=e1 in e2R(λx:T.u)u
SI-LetEx
Γe1Tuy freshΓ,?y:Te2Ru
Γlet ?:T=e1 in e2R(λy:T.u)u
SI-LetIm
ΓeRu
ΓeRu
SI-Stitch

The checking query in SI-ImpE is therefore a SI-Query synthesis followed by SI-Stitch when its type is restricted. Crucially, SI-Query itself permits any matching implicit binding. Lexical priority is imposed on complete derivations by definition 16.16. Declaratively, SI-AllE may instantiate X.T with any well-formed S. Algorithmically, synthesis creates a fresh metavariable α, elaborates the consumer, and solves α when an application or checking judgment next constrains its expected type.

For the opening SI term, write the three implicit variables as i1,if,i2. The definition of f checks as ?i1:Int,?if:Int?Intif, so implicit introduction gives fλif:Int.if. At the final use, implicit elimination generates a query at Int; the rightmost eligible binding is i2. The complete target therefore contains (λif:Int.if) i2i2, and the enclosing explicit lets substitute 2 for i2.

Definition 16.16 — Well-scoped SI derivation

A typing derivation is well scoped when every query selects the rightmost eligible implicit entry. If a query subderivation D ends in SI-Query, write res(D) for that variable. Formally, for every query subderivation D and every other derivation D of the same query judgment, either res(D)=res(D), or res(D) is defined to the left of res(D) in Γ. This condition resolves shadowing; it does not guarantee termination when eligible values themselves require implicit arguments.

The derivation that selects the earlier binding i1 in the opening shadowing context is not well scoped. If D1 selects i1 and D2 selects the later eligible i2, then res(D1)=i1,res(D2)=i2,i1 lies to the left of i2. The defining comparison therefore fails for D1 and succeeds for D2.

Theorem 16.17 — Published SI results

For the SI calculus of [OBL^+18]:

  1. the translation of a closed well-typed SI term into System F preserves its translated type;

  2. in the monomorphic fragment without polymorphic function types, a term has at most one well-scoped synthesis derivation at a restricted type, and checking a term against a given type has at most one well-scoped derivation;

  3. if the published synth procedure returns a type and target, the corresponding synthesis derivation exists; if check returns a target at a supplied type, the corresponding checking derivation exists; and

  4. semi-completeness for terms with no query in synthesis position is stated as a conjecture, with divergence permitted.

Proof of Theorem 16.17 — Published SI results

Imported proof. Source. Items 1–3 are Theorem 3.1 and Propositions 3.6–3.8 of [OBL^+18], specialized to the SI calculus fixed above. For example, SI-ImpE combines a function of type ST with the checked query of type S. Item 4 records Conjecture 3.9, not a proved result. The monomorphic restriction and possible divergence are retained. ◻

This calculus answers the section’s question. An implicit function arrow elaborates to an ordinary function arrow, so it is dictionary-like in the weak sense that omitted values become explicit parameters. It differs in what those values mean and how they are found. SI searches lexical values by type and shadowing order. MTC0 and modular type classes construct class modules indexed by a distinguished carrier. Modular implicits search ordinary module expressions constrained by module types and type equations. The target may look like argument passing in all three cases, but the source static disciplines are not interchangeable.

Exercise 16.9

★★☆ Elaborate and reduce let ?:Int=10 inlet add:Int?(IntInt)=λx.x+? inlet ?:Int=3 in add 4. Indicate separately the query resolved while checking the body of add and the query generated by automatic implicit elimination at its use. Explain why only one of them observes the inner binding 3.

The boundary between the three mechanisms

The chapter has used three different evidence disciplines.

Mechanism Implicit object Search key Scope rule
MTC0 and modular classes class module class name and carrier head top-level using declarations
Modular implicits module expression module type and type equations lexical implicit-module space
SI implicit functions ordinary value expected value type rightmost eligible lexical binding

All three elaborate omission into explicit target syntax. That shared last step is not enough to identify their coherence, termination, ambiguity, or abstraction theorems. A theorem transfers only through an explicit translation preserving the hypotheses that make resolution meaningful.

Sources and exact limits.

The modular-type-class development, including its declarative elaboration and inference-soundness theorem, is due to Dreyer, Harper, Chakravarty, and Keller [DHCK07]. Modular implicits and their archived OCaml prototype are due to White, Bour, and Yallop [WBY15]; their paper presents the design and elaboration but not a complete formal inference/coherence metatheory for OCaml. The SI calculus and the exact results in theorem 16.17 are due to Odersky, Blanvillain, Liu, Biboudis, Miller, and Stucki [OBL^+18]. Stable coherent implicits provide a useful neighboring calculus, but its theorems require its own stability and unambiguity conditions and are not silently inherited here [SdSOWM19].

Suggested first pass.

Problems exercise 16.10, exercise 16.11, exercise 16.14 form the suggested first pass.

Exercise 16.10

★★☆ For this exercise, extend MTC0 as follows. A class may contain finitely many named associated type components in addition to its distinguished carrier t; every declaration gives each component a transparent constructor expression. Resolution is still keyed only by the class and carrier head (K,c), so non-overlap is unchanged, while field selection recovers the associated equations from the selected module. Use the extended class signature COLLECTION:=sig {type t;type Elem;val empty:t;val insert:Elemtt}. Give modules for integer lists and integer sets that have the same associated component Elem=Int but different carrier types. Explain why the class cannot be represented by the single dictionary type ttBool used for equality in chapter 11. Then state the realized signatures of both modules and show how transparent access to Elem type-checks one insertion call for each.

Exercise 16.11

★★★ Prove the converse limitation of corollary 16.7: if an extension is permitted to replace the declaration at a result head occurring in an old evidence derivation, resolution need not be stable. Give the smallest counterexample. Then, for finite admissible environments viewed as maps from result heads to declarations, formulate and prove a stronger stability statement in which an update may rewrite entries at heads outside the old derivation, provided every entry used by that derivation remains syntactically identical.

Exercise 16.12

★★☆ A modular-implicit search space contains OrdToEq,HashToEq,OrdInt,HashInt. The first two are functors; the latter two provide ordering and hashing for integers. Both functor applications produce modules matching EQ with carrier Int. Draw the two evidence paths for a call requiring integer equality. State one additional premise under which alias equivalence could collapse the paths, and explain why extensional agreement of the two eq functions alone is not a decidable module-alias test.

Exercise 16.13

★★★ The uniqueness propositions imported for SI exclude polymorphic function types. Construct two distinct typing and elaboration choices made possible by a polymorphic implicit value, or reconstruct the counterexample from the source. Identify the exact step at which the monomorphic uniqueness induction no longer determines one restricted synthesized type. Your answer must separate failure of uniqueness from possible divergence of the search algorithm.

Exercise 16.14

★★☆ For each requirement below, select MTC0/modular type classes, modular implicits, SI implicit functions, or explicit module passing, and justify the choice from the formal search and scope rules rather than surface syntax.

  1. two local pretty-printing configurations for the same carrier, selected at different call sites;

  2. an associated output type that must remain abstract behind a module boundary;

  3. a lexically shadowed integer tolerance used by ordinary functions;

  4. a security-sensitive dependency for which no search or ambiguity is acceptable; and

  5. automatic structural construction of equality evidence for nested products and lists with one available instance per constructor head.

For one item, give a plausible second choice and a concrete reason it is worse.

Exercise 16.15

★★★ Practical project.mtc-resolution Implement the four-constructor running sublanguage of the finite calculus in definition 16.2 and the resolver of definition 16.5. The algorithm must validate declarations before search, then perform head-directed recursive resolution. Maintain this invariant: every recursive request is at a proper type subterm of its caller, and every returned evidence tree type-checks against the requested realized signature.

The concrete observable result is a command-line report containing validation, resolution, and elaboration outcomes. The decidable acceptance test must check all of the following exact cases:

  1. the standard environment is accepted;

  2. resolving the request EQ[List(Pair(Int,Bool))] prints EqList(EqPair(EqInt,EqBool));

  3. elaborating equality at that type prints the same evidence followed by .eq;

  4. adding a second EQ[Int] declaration is rejected with overlap;

  5. omit the declaration for SHOW[Bool]; resolving that request is rejected with missing; and

  6. the declaration EqAgain:EQ[α]modEQ[α] is rejected with nondecreasing.

The inline kappa test oracle must exit unsuccessfully if any expected output differs; kappa run prints the diagnostic report and is not the process-level rejection oracle. The companion directory is

artifacts/ch16-mtc-resolution/.

Search the book

Type to search the local edition.