Lectures onType Theory
Chapter 31
Chapter 31Core route

Effect Rows, Principal Type-and-Effect Inference, and Handlers

Chapter 22 recorded effects in finite sets. A set says which operations may occur, and it is the right abstraction for the semantic arguments made there. But it forgets how many copies of a label a type contains. That loss is harmless until a handler, an operator that interprets selected requests, removes one occurrence of a label while permitting another occurrence to escape.

Effect rows retain those occurrences as finite multisets with a possibly open tail. Thus raise,raiseμraiseμ. Read ε as the label consed onto the row ε; the vertical bar separates the visible prefix from its tail. The duplicate is not decoration. A handler can consume the first occurrence and leave the second in its result effect. The same convention also gives a small, unconstrained unification algorithm: there is no separate “lacks” predicate saying that a tail does not contain a given label.

The transaction whose tail stays open

Fix the operation declarations used by the transaction. get:1Int,put:Int1,raise:Error0,log:Error1. For a callback f, use the core computation; the saved value s witnesses the initial read, while the callback’s result is committed by put: tf=perform get () to s.f () to x.perform put x to u.return x. The callback has different principal signatures when it is let-polymorphic and when it is a first-class monomorphic argument. Put (31.1)Eμ=get,put,raiseμ,Gμ=get,putμ. Write τ!ε for the type of a computation that returns a value of type τ and performs the effects listed in the row ε; in chapter 22 the same separator instead carried a finite set of operations. If Γf=f:ν.1raiseνInt, then the occurrence of f in tf instantiates ν=get,putμ, and (31.2)Γftf:Int!Eμ,genc(Γf,Int!Eμ)=μ.(Int!Eμ). Here genc(Γ,τ!ε) quantifies every type and row variable free in τ!ε but not in Γ. If instead we form the value λf.tf, its parameter is monomorphic, so its principal scheme is (31.3)μ.(1GμInt)GμInt. The lambda-bound f has no declared raise effect; only the get and put actually performed by tf remain. The row Eμ is a legal instance of this more general scheme, obtained by taking the tail of G to begin with raise, but it is not principal for the wrapper. Giving a first-class argument its own quantified row scheme would require higher-rank row polymorphism, which this Hindley–Milner core does not have. In both signatures μ is open, so callers may add logging or allocation effects.

Now compare two exception handlers. A fallback handler applied at the principal instance Eμ removes raise and leaves get,putμ. A rethrowing handler must instead leave one raise in its output. It therefore asks for two occurrences in its input, one to consume and one to re-perform. The principal row of tf admits that instance by taking μ=raiseν. Its input row is then get,put,raise,raiseν; the rethrowing handler removes one occurrence and leaves get,put,raiseν for an outer handler.

Rows are multisets, not sets

Definition 25.1 — Effect rows and equivalence

Let labels range over a fixed finite operation signature Σ, with Σ()=PR. Every declared P and R is a closed, well-kinded type; in particular, type or row substitution never changes an operation declaration. Type variables are written α, and row variables μ. Kinds keep the two sorts apart. κ::=R,α::,μ::R. The kind classifies ordinary value types and R classifies effect rows. A substitution is kind preserving when it maps -variables to types and R-variables to rows. τ::=αbτετ,ε::=με,σ::=α¯μ¯.τ,χ::=α¯μ¯.(τ!ε). We abbreviate nested extensions by 1,2ε. A closed row has no row variable. It is duplicate-free when no label occurs twice. The base types b include 0,1,Bool,Int, Error, and Env. Contexts assign value schemes σ to variables. A computation scheme χ records both inferred outputs; it is used to state principality, not as a context entry. Constants range over a fixed signature C. Every declaration C(c)=α¯μ¯.τ is closed and well kinded: all free variables of τ are bound by the displayed quantifiers. Define ftv(τ!ε)=ftv(τ)ftv(ε),genc(Γ,τ!ε)=(ftv(τ!ε)ftv(Γ)).(τ!ε),genv(Γ,τ)=(ftv(τ)ftv(Γ)).τ.

The row equivalence is the least congruence generated by adjacent exchange: (31.4)1,2ε2,1ε. Here is finite-multiset equivalence of effect rows and its congruential extension to the displayed types and contexts; it is not the book’s judgmental-equality relation. There is deliberately no contraction equation. Equality of closed rows is therefore equality of finite multisets. A substitution is kind preserving; its application is homomorphic and row equality is always read modulo . No side condition 12 is needed in the exchange equation: when the two labels agree, it is simply an identity. Extend congruentially to types: base types and variables are equivalent only to themselves, and A1ε1B1A2ε2B2 exactly when A1A2, ε1ε2, and B1B2. Context equivalence is pointwise on the bodies of schemes, after alpha-renaming bound variables. Thus TΓQSΓ below is a pointwise statement, not literal syntax equality.

For a row ε, write M(ε):ΣN for the multiplicity function of its visible prefix. A variable or empty tail contributes zero, and + denotes pointwise addition. This notation will support both cancellation and the unification guard.

Lemma 25.2 — Cancellation

For a label , ε1ε2ε1ε2. More generally, if L and Lεδ, then εε for some ε.

Proof of Lemma 25.2 — Cancellation

Proof. Every row has a normal form Lt, where L is a finite multiset of visible labels and the tail atom t is either one row variable or . Bubble-sorting adjacent labels in a fixed total order proves LtLtL=L as multisets and t=t. Exchange leaves the multiplicity function M and the tail atom unchanged, so equivalent rows have equal multiplicities and equal tails; bubble sort gives the converse. The first claim subtracts one from the multiplicity of on both sides. For the second, the right side has positive -multiplicity and the prefix L contributes none, so the tail contributes at least one. Exchange moves that occurrence to its head. ◻

Unique-label rows require an absence constraint such as μ when unifying μ. Here duplicates are legal, so ordinary substitutions suffice.

The two row systems have the following exact division of labor.

record rows of chapter 4 effect rows here
duplicates forbidden retained
tail obligation μ common-tail guard in the unifier
equality exchange plus uniqueness multiset exchange
exposure insert with lacks rewrite without lacks
elimination select one unique field handle one occurrence

The two mechanisms prevent the same cyclic exposure in different places: Chapter 4 records a lacks predicate in the type, whereas this chapter checks a common-tail guard during unification. See also the scoped-duplicate comparison in subsection 7.8.2; effect multiplicity is a deliberate equational choice, not a novel exposure algorithm. Swapping the equalities is unsound: contraction would erase the distinction between a handled occurrence and a re-performed one, while allowing duplicate record labels would make field selection ambiguous.

Exercise 25.1 — No contraction

★☆☆ Give a multiplicity model separating ,μ from μ. Explain why a handler that re-performs makes the distinction observable at the type level.

A fine-grain call-by-value core

Definition 25.3 — Fine-grain effect-row calculus

Values and computations are separate: v::=xcλx.e,e::=return vvve to x.eperform  vhandle e with Hlet x=e in e,H::={return xer; i(pi;ki)ei}i=1n. This separation of syntactic value forms from computations, together with elimination forms that take values, is what fine-grain call by value means here. Unlike CBPV in chapter 22, this core has no thunk/force type boundary. The handled labels 1,,n are distinct. The judgments are Γvv:τ and Γe:τ!ε. Instantiation at a variable is implicit. Constants c are base constructors with fixed closed schemes; all function values in the core are lambdas. For H={return xer;i(pi;ki)ei}i=1n, write handled(H)={1,,n}.

x:α¯μ¯.τΓT instantiates only α¯,μ¯
Γvx:Tτ
V-Var
C(c)=α¯μ¯.τT instantiates only α¯,μ¯
Γvc:Tτ
V-Const
Γ,x:τ1e:τ2!ε
Γvλx.e:τ1ετ2
V-Lam
Γvv:τ
Γreturn v:τ!ε
C-Return
Γvv1:τ1ετ2Γvv2:τ1
Γv1v2:τ2!ε
C-App
Γe1:τ1!εΓ,x:τ1e2:τ2!ε
Γe1 to x.e2:τ2!ε
C-To
Σ()=PRΓvv:P
Γperform  v:R!ε
C-Op

Only C-Return and C-Op may choose their row tails freely. There is no general effect-weakening rule; in particular, an application has exactly the latent row written on its function type. Because C-Return and C-Op may choose any tail, both arms of a sequence can be typed at one common effect. Let-generalization is restricted to pure computations:

Γe1:τ1!a¯=ftv(τ1!)ftv(Γ)Γ,x:a¯.τ1e2:τ2!ε
Γlet x=e1 in e2:τ2!ε
C-Let

Here a¯ contains variables of both kinds. Effectful sequencing uses to, whose bound variable is monomorphic. This is also the surface elaboration policy. A source let is accepted as the core let form only when inference can unify the bound computation’s row with ; an effectful source binding must instead elaborate to core to, and is monomorphic. Thus the core syntax records, rather than retrospectively guesses, the generalization decision.

The sole conversion rule changes only the row annotation:

Γe:τ!εεε
Γe:τ!ε
C-RowConv

Hence all typing rules are stable under row equivalence. In particular, the effect in C-To need only be equivalent in its two premises; this is a consequence of C-RowConv, not a second conversion convention.

Rule C-Let is already visible on a closed term. If Γv0:Int, write C0=Int!,e0=let x=return0 in returnx. Then Γv0:IntΓreturn0:C0CReturnftv(C0)ftv(Γ)=Γ,x:Intvx:IntΓ,x:Intreturnx:C0CReturnΓe0:C0CLet. The generalized-variable set is empty here; replacing (0) by a polymorphic pure value exercises the same rule with a nonempty set.

Definition 25.4 — Handler typing

The handler rule removes exactly one occurrence of each handled label.

Γe:A!1,,nεΓ,x:Aer:B!ε(Σ(i)=PiRi)i=1n(Γ,pi:Pi,ki:RiεBei:B!ε)i=1n
Γhandle e with H:B!ε
C-Handle

The continuation in a clause is deep: invoking it resumes under the same handler. If a clause performs its own i, its output row ε contains i. The premise for the handled computation then contains two copies.

Here is the calculation for exceptions. Fix a result type B, a fallback d:B, and a row ν. Define CdB={return xreturn x;raise(p;k)return d},RB={return xreturn x;raise(p;k)perform raise p to z.kz}. The fallback handler has output row ν. Its operation-clause premise is d:B,p:Error, k:0νB  return d:B!ν. Consequently, if e:B!raiseν, then handle e with CdB:B!ν.

The rethrower has output row ε=raiseν. Its operation clause uses the empty response only after the re-performed request: Σ(raise)=Error0p:Error, k:0εBvp:Errorp:Error, k:0εBperform raise p:0!εCOpp:Error, z:0, k:0εBkz:B!εp:Error, k:0εBperform raise p to z.kz:B!εCTo. Thus RB has input row raise,raiseν and output row raiseν. No value of 0 has been invented: z is bound by the sequencing form, and the continuation call lies after the re-performed request. The fallback trace below discards that continuation instead of manufacturing a response.

This typing predicts the reduction. For a handler-free request context R, put q=R[perform raise p], and assume q:B!raise,raiseν. Rule C-Op may choose any tail; here its tail already contains one raise, while the displayed request contributes the other. The inner handler consumes the displayed occurrence and its clause re-performs the one that remains visible. The inner handler rethrows outside itself: handle q with RBperform raise p to z.(λy.handle R[return y] with RB)z. An outer fallback handler captures this request. Its clause discards the rebuilt continuation, so handle(handle q with RB) with CdB2return d. At the type level the two handlers remove the two occurrences in succession: B!raise,raiseνRBB!raiseνCdBB!ν.

A logging rethrower prefixes the clause of RB by perform log p to u. With εL=log,raiseν, both operations and the continuation call have effect εL, modulo exchange. Its input is therefore raise,log,raiseν, and its output is εL. Applied to the transaction, the tail in Eμ is instantiated to provide the second raise and the log; get and put remain in the ambient tail throughout. More explicitly, put EξL=get,put,log,raiseξ. The logging rethrower takes Int!raiseEξL to Int!EξL, while the required instance of the transaction is obtained from Eμ by the row calculation Eμ[log,raiseξ/μ](25.1)get,put,raise,log,raiseξ(25.4)raiseget,put,log,raiseξraiseEξL. The handler removes the first displayed occurrence and leaves exactly EξL. Equivalently, within εL take ν=get,putξ.

Exercise 25.2 — Typing the two catches

★★☆ Instantiate the displays above with B=Int and ν=get,putξ. Write both complete instances of C-Handle, including the return-clause premises, and display the effect before and after each handler.

Operational meaning

Definition 25.5 — Operational semantics

Evaluation contexts and handler-free request contexts are generated by E::=[]E to x.elet x=E in ehandle E with H,R::=[]R to x.elet x=R in e. A let-shaped request context is included so the context grammar is closed under the term syntax. It is unreachable in a well-typed terminal request: C-Let requires its bound computation to have the empty row, so that position cannot expose an operation. A request context R contains no handler frame. Consequently the handler displayed at a root is the nearest syntactic handler around the request: it either handles that label or forwards the request across exactly one handler layer. The same grammar also describes a terminal request context when no handler encloses it. Besides beta and sequencing, the roots are (λx.e)v0e[v/x],(return v) to x.e0e[v/x],let x=return v in e0e[v/x],handle (return v) with H0er[v/x],handle R[perform i v] with H0ei[v/pi,(λy.handle R[return y] with H)/ki]. If is not handled by H, forwarding preserves the operation and rebuilds its continuation: handle R[perform  v] with H0perform  v to y.handle R[return y] with H. The last root in the first display is H-Op; the forwarding root is H-Forward. The reduction relation is the least relation containing these roots and closed under E. Their label tests are complementary. Because R has no handler frame, a nested request first meets its innermost handler; context closure cannot also contract an outer handler root. Repeated H-Forward steps give the familiar derived “open context” behavior, but that multi-layer behavior is not a primitive reduction.

The active spine of E is the ordered list of frames encountered from its hole to its root.

Lemma 25.6 — Nearest-handler decomposition

For every evaluation context E, request label , and value v, exactly one of the following decompositions applies:

  1. E[perform  v]=R[perform  v] for a unique request context R;

  2. there are unique E, R, and H such that E[perform  v]=E[handle R[perform  v] with H], and the displayed occurrence of H is the innermost handler frame on the active spine.

In the second case, membership of in handled(H) selects exactly one of H-Op and H-Forward.

Proof of Lemma 25.6 — Nearest-handler decomposition

Proof. Read the frames of E from the hole outward. If there is no handler frame, all frames belong to the grammar of R, giving the first decomposition. Otherwise split the frame list immediately after its first handler. The inner prefix is the unique R, that first handler is the unique H, and the remaining suffix is the unique E. The two cases are disjoint by the presence or absence of a handler frame, and the two label tests are complementary. ◻

Exercise 25.3 — Forward exactly one layer

★★☆ Let R be handler free, let handled(H0), and let H1 contain the clause (p;k)e. Starting from handle(handle R[perform  v] with H0) with H1, write the H-Forward step through H0 and the next H-Op step at H1, including both rebuilt continuations. Give the decomposition from Lemma 25.6 before each step, and explain syntactically why the outer H-Op is not a root of the initial term.

Lemma 25.7 — Replacement for request contexts

Suppose ΓR[e]:B!ε, and the derivation assigns Γe:A!δ at the hole. If Γe:A!δ, then ΓR[e]:B!ε.

Proof of Lemma 25.7 — Replacement for request contexts

Proof. Induct on R. A sequencing frame uses the induction hypothesis in the left premise of C-To; the right premise is unchanged. A let frame uses it in the first premise of C-Let. Sequencing and let are exactly the two constructors of a request context. ◻

Write σσ when every kind-correct instance of σ is an instance of σ, read “σ is at least as general as σ” exactly as in chapter 3.

Lemma 31.8 — Type-and-row action and scheme enlargement

If ΓJ is a value or computation judgment and S is kind preserving, then SΓSJ, after renaming scheme binders away from dom(S). If σσ, replacing an assumption y:σ by y:σ preserves a derivation.

Proof of Lemma 31.8 — Type-and-row action and scheme enlargement

Proof. Induct on the derivation. The variable case composes the selected instance with S; the constant case does the same within its closed declaration; C-Op is unchanged because every P,R is closed; and C-RowConv uses the fact that kind-preserving substitution preserves . In C-Let, the variables later bound by generalization are still free in the bound monotype. Apply one finite kind-preserving bijection to the entire bound-expression premise and to the body’s generalized declaration. This is licensed because those variables are fresh for Γ, and it makes them fresh for both the domain and range variables of S; it is not merely an alpha-renaming of the scheme prefix. The scheme recomputed from SΓ,SA1 is at least as general as the pointwise image of the old scheme. Put Q=ftv(A1)ftv(Γ) and choose the variables of Q fresh for the domain and range of S. Then S fixes every member of Q, none occurs in SΓ, and Qftv(SA1)ftv(SΓ). Thus every instance of S(Q.A1) is an instance of gen(SΓ,SA1). Scheme enlargement therefore derives the body premise from its old instance. The handler case applies the induction hypotheses under the renamed return, parameter, and resumption binders. All binders are renamed fresh before an induction hypothesis is used. Scheme enlargement itself changes only V-Var: an instance admitted by σ is still admitted by σ. ◻

Lemma 25.8 — Type, row, ordinary, and generalized substitution

If Γ,x:A,Δvw:B and Γvv:A, then Γ,Δvw[v/x]:B. If Γ,x:A,Δe:B!ε, then Γ,Δe[v/x]:B!ε.

Proof of Lemma 25.8 — Type, row, ordinary, and generalized substitution

Proof. Use a simultaneous induction on value and computation typing. At V-Var, the distinguished variable uses the premise for v, while every other variable is unchanged. In V-Const, the declared instance is unchanged. In V-Lam, rename the lambda binder away from x and the free variables of v, then apply the computation induction hypothesis below it. C-Return, C-App, and C-To apply the appropriate hypotheses to their immediate subderivations. In C-Op, substitute in the parameter value; the closed declaration types remain literally P,R. The C-RowConv case reapplies the same row equivalence.

For C-Let, rename its binder y away from x,v. The induction hypothesis types the substituted bound computation. Removing x:A from the surrounding context can only enlarge ftv(A1)ftv(Γ). Hence the new scheme σ1 computed by C-Let is at least as general as the old σ1: σ1σ1. Scheme enlargement retags the body derivation with y:σ1, and its induction hypothesis substitutes for x. In C-Handle, alpha-rename the return binder and every parameter and resumption binder pi,ki. Apply the induction hypotheses to the body, the return clause under its value binder, and every operation clause under both its parameter value binder and its resumption value binder. Their arrow and row annotations are unchanged, so C-Handle reconstructs the conclusion. ◻

Lemma 31.10 — Generalized value substitution

If a¯ftv(Γ)=, Γvv:A, and Γ,x:a¯.A,ΔJ, then Γ,ΔJ[v/x]. Each occurrence of x may choose a different kind-correct type-and-row instance of a¯.

Proof of Lemma 31.10 — Generalized value substitution

Proof. Repeat the simultaneous induction of lemma 25.8. Its only new variable case is an occurrence of x at an instantiation TA. Freshness gives TΓ=Γ, so lemma 31.8 derives Γvv:TA. Lambda, operation, conversion, let-scheme, return, parameter, and resumption binders use exactly the cases just enumerated. Thus separate occurrences may instantiate a¯ differently without changing the substituted term. ◻

Lemma 31.11 — Arrow canonical form

If vv:AεB, then v=λx.e for some x,e.

Proof of Lemma 31.11 — Arrow canonical form

Proof. Invert value typing. The empty context excludes V-Var, and every V-Const instance has the outer constructor of its closed base declaration. Only V-Lam concludes an arrow type. ◻

Theorem 25.9 — Preservation

If Γe:A!ε and ee, then Γe:A!ε.

Proof of Theorem 25.9 — Preservation

Proof. The beta and sequencing roots use ordinary substitution from lemma 25.8. For the let root, inversion of its pure-return premise gives Γvv:A, and the variables generalized by C-Let are fresh for Γ; use lemma 31.10. A handled return uses ordinary substitution in the return clause.

For a handled operation, invert the typing of the request context. Its hole has type Ri!δ. Rule C-Return assigns the same hole effect to return y; lemma 25.7 reconstructs the handler premise, and C-Handle gives the captured continuation type RiεB. First substitute the request parameter v:Pi for the clause’s value binder pi; then substitute the rebuilt continuation value for its resumption binder ki. Lemma 25.8 gives both steps, including any nested lambdas, lets, operations, handlers, or row conversions in the clause.

For forwarding, inversion gives 1,,nεδ,{1,,n}. Lemma 25.2 yields εε. Replacement types the rebuilt continuation at ε, and C-Op followed by C-To types H-Forward at the same effect. Contextual cases apply the induction hypothesis to the active premise; C-RowConv reapplies its unchanged equivalence. ◻

An exposed terminal request is R[perform  v]. It contains no handler frame: a request immediately inside a handler either takes H-Op or takes H-Forward, while a more deeply nested request first reduces at its innermost handler. Thus an exposed terminal request is an observable request, not a runtime fault and not a reducible forwarding configuration.

Theorem 25.10 — Progress and absence of unhandled operations

If e:A!ε, then exactly one of the following holds: e=return v, the computation takes a step, or it is an exposed terminal request whose label occurs in ε. Hence a closed computation of effect cannot expose an operation.

Proof of Theorem 25.10 — Progress and absence of unhandled operations

Proof. Induct on typing. Lemma 31.11 settles application. For sequencing, apply the induction hypothesis to the first computation: a return takes the sequencing step, a reduction lifts, and an exposed operation remains exposed in the enlarged terminal request context. The let case is similar, except that its first computation has empty effect: an exposed-operation outcome is impossible, a return takes the let step, and a reduction lifts through the let frame. For a handler, a returned value uses its return rule; a reduction lifts; a request for a handled label uses H-Op; and every other request uses H-Forward. It is therefore a step rather than a terminal-request outcome. For C-RowConv, apply the induction hypothesis to its premise. A return or reduction is unchanged. In the exposed-request alternative, row equivalence preserves every label multiplicity, so occurrence of the exposed label in the premise row implies its occurrence in the converted row. Inversion of C-Op and repeated cancellation show that the exposed label occurs in the concluding row. The empty row has multiplicity zero for every label. Finally, the three outcomes are disjoint: returns and terminal requests have different outer forms, and a terminal request has no redex. For a step, the handler-free grammar of R and the ordinary call-by-value forms give unique active decompositions. In the request case lemma 25.6 gives the unique nearest root, where the handled-label test selects exactly one of H-Op and H-Forward. ◻

Exercise 25.4 — The forwarding case

★★☆ Fill in the forwarding case of preservation without writing “by weakening.” Name the row equivalence obtained by inversion, apply the second part of lemma 25.2, and derive the type of the continuation rebuilt in H-Forward.

Unification by exposure

The ordinary first-order unification clauses handle type variables, constants, and arrows. The row clause exposes a requested label at the head and returns a most-general substitution.

Write rewrite(ε,)=(S,ε) when the operation succeeds. Its contract is (31.5)SεSε. This is the constrained insertion operation of chapter 4 with its lacks premise removed: both operations expose one requested label and return the residue, but duplicate effect labels make absence evidence unnecessary here. It is defined by the following equations. In Rw-Var, choose ν outside the row variables in the entire unification problem and every substitution constructed so far. (31.6)rewrite(ε,)=(id,ε)RwHeadrewrite(ε,)=(S,ε)RwSkip,rewrite(ε,)=(S,ε),rewrite(μ,)=([μν],ν)RwVar. Rewriting the empty row fails. In Rw-Skip, the returned substitution also acts on the residual row.

The unifier state is a list of pending equations and an accumulated substitution. Write U(A,B) for the result from initial state ([AB],id). After a clause returns S1, every residual equation is replaced by its S1-image before the work list continues; the arrow and row-extension displays spell out those states.

The unifier tries cases in the fixed order used by definition 25.11: reflexivity, variable orientation, empty and constructor clauses, and only then row extension. Hence the following clause does not compete with U-Var. Let tail(ε) be the final row variable, when one exists. The row-extension clause of unification is (31.7)U(ε1,ε2):(S1,ε3):=rewrite(ε2,);if tail(ε1)dom(S1) then fail;S2:=U(S1ε1,S1ε3);return S2S1. The membership test is false when the left row has no variable tail.

Definition 25.11 — All unification cases

Unification is called only on two objects of the same kind and preserves that kind. Its type objects are exactly α, b, and AεB; its row objects are exactly μ, , and ε. In the following display, a ranges over variables of either kind, X over an object of that same kind, and b over base types: (31.8)U(a,a)=idUReflU(a,X)=[aX]UVar,aftv(X), aXU(X,a)=U(a,X)USym,X is not a variableU(b,b)=idUBaseU(,)=idUEmpty. Rule U-Var fails its occurs check when aftv(X). After reflexivity and the variable clauses, the complete same-kind clash table is (31.8a)U(b,b)=failUClashBase,bbU(b,AεB)=failUClashBaseArrowU(AεB,b)=failUClashArrowBaseU(,ε)=failUClashEmptyExtendU(ε,)=failUClashExtendEmpty. A proposed type–row equation is rejected as ill kinded before U is called; it is not another same-kind clash case.

For arrows, the complete clause is (31.9)S1:=U(A1,A2);S2:=U(S1ε1,S1ε2);S3:=U(S2S1B1,S2S1B2);U(A1ε1B1,A2ε2B2):=S3S2S1.UArrow Rows headed by a label use (25.7), called U-Extend; a variable or empty row on either side has already been covered by U-Refl, U-Var, U-Sym, U-Empty, or one of the five displayed U-Clash cases. These cases are disjoint after trying reflexivity and variable binding first, so the displays define the whole algorithm at both kinds.

The guard in (25.7) is load bearing. Without it, unifying (31.10)1μwith2μ,12. first exposes 1 on the right by proposing S1=[μ1μ1],ε3=2μ1,1μ12μ1. The recursive equation is (25.10) again with fresh tail μ1. Each round consumes one tail variable and produces the next, so the substitution grows without bound. With the guard the original equation fails. It has no finite-row solution: cancelling the common multiset denoted by μ would assert {1}={2}.

Lemma 25.12 — Exposure

If rewrite(ε,)=(S,ε), then (25.5) holds. Moreover, if Tεδ, then rewriting succeeds and, for every finite protected set Z of variables, its fresh ν may be chosen outside Z and there is a substitution Q such that (31.11)Q agrees with T on Z,T=QSon Zftv(ε),δQε. Thus exposure is most general.

Proof of Lemma 25.12 — Exposure

Proof. For soundness, induct on (25.6). The head case is reflexivity. The skip case applies the induction hypothesis below the skipped label and then exchanges past . In the variable case, substitution gives ν directly.

For factorization, induct down the visible prefix of ε. If its head is , cancel it and take the identity factor. If ε=ε1 with , the recursive rewrite returns ε1, and the second clause of lemma 25.2 gives Tε1δ; its first clause then gives δδ. The induction hypothesis gives δQε1, and congruence restores the skipped . If the tail is μ, the assumed equivalence says that Tμ contains ; exchange that occurrence to the head and write Tμρ. Choose νZ, define Qν=ρ, and put Qα=Tα for every αν. Then T=Q[μν] on Zftv(ε), and Q agrees with T on Z. The empty case contradicts the assumed positive -multiplicity. ◻

Lemma 25.13 — Termination

Both rewrite and U terminate on finite types and finite rows.

Proof of Lemma 25.13 — Termination

Proof. Rewriting either stops or calls itself on a proper row tail. For a row ε, let |ε| be the number of visible label constructors; a variable or empty tail contributes zero. Consider the recursive call made by (25.7). If rewriting finds in the visible prefix of the right row, that call has removed the left head and the exposed right occurrence. The sum of the two visible sizes decreases by two.

Otherwise the right row is Lμ, where L has no , and Rw-Var binds μν. The guard says that μ is not the tail of ε1; since a row has only one tail, μ does not occur in ε1. If |ε1|=m and |L|=n, the old extension equation has visible size 1+m+n, while its recursive tail equation has size m+n. Thus every recursive row-extension call strictly decreases a natural number. Variable, empty, and equal-head cases either stop or enter that decreasing call; both empty–extension clash orientations stop immediately. Thus row unification terminates.

The recursive U-Arrow presentation and the work-list presentation agree: its three recursive calls push the domain, effect, and codomain equations in that order, composing each returned substitution into the remaining list. For that work-list representation, let C be the finite list of equations remaining after applying the accumulated substitution. Let v(C) count distinct unsolved variables of either kind, let c(C) count all base and arrow constructors occurring in the equations, and let n(C) count equations. Use the lexicographic measure (31.12)M(C)=(v(C),c(C),n(C))N3. Rows contain no base or arrow constructors. A binding of a row variable therefore cannot duplicate anything counted by c(C); it changes only the row-specific measure handled by the atomic row procedure. A U-Var step permanently removes its variable and introduces no fresh outer-work-list variable, so the first component decreases even if applying the binding duplicates constructors. A U-Arrow step keeps the first component fixed and removes the two arrow heads. Its domain, effect, and codomain constructors already occurred below those heads, so the second component decreases. A successful U-Base step also decreases the second component. A U-Refl step removes an equation. Each base–base or base–arrow clash stops immediately. A row-unification call is the terminating atomic procedure proved above. Every fresh tail introduced by exposure replaces one bound old tail, so this call does not increase v; if v stays fixed, removing the solved row equation without adding an outer equation decreases n. Thus every outer transition decreases M, and the complete unifier terminates. ◻

Theorem 25.14 — Most-general type-and-row unification

If U(A,B)=S, then SASB. Conversely, if TATB, the algorithm succeeds with some S, and there is a Q such that T=QS on the variables of A,B.

Proof of Theorem 25.14 — Most-general type-and-row unification

Proof. Soundness is by induction on a successful unifier derivation; the five clash cases have no successful conclusion. If a variable α is bound to B, the occurs check has established αftv(B), and the returned substitution makes both sides literally B. Equal constants return the identity. For arrows, the domain hypothesis equates the domains; the effect hypothesis equates the two effects after that substitution; and the codomain hypothesis equates the codomains after both earlier substitutions. Congruence then equates the arrows. For (25.7), Lemma 25.12 gives S1ε2S1ε3; the recursive hypothesis equates the tails after S2, so congruence equates the original rows after S2S1.

For completeness, strengthen the induction statement to factorization. In a variable equation αB, any unifier T satisfies Tα=TB. Define Q to agree with T away from α; then T=Q[αB] on the variables of the equation. If αftv(B) and Bα, substitution gives an equation in which Tα contains itself beneath at least one constructor. Exchange preserves the number of row-extension constructors, and type congruence preserves all other constructor counts, so this equation would imply |Tα|1+|Tα|, impossible for a finite type. Thus no unifier exists. The constant case is reflexive. Distinct bases cannot be unified; a base and an arrow have different outer type constructors; and an empty row cannot equal an extension because their multiplicities are respectively zero everywhere and positive at the head label. These arguments cover both displayed orientations, so every same-kind U-Clash failure is complete. A type–row pair is not a well-kinded equation. For arrows, factor the given unifier through the domain MGU, apply the residual factor to the substituted effect equation, and then to the substituted codomain equation. The three induction hypotheses compose in exactly that order.

In the extension case, apply the factorization half of lemma 25.12 to the occurrence of on the right, taking Z=ftv(ε1). The protected-set clause produces T=Q1S1 on every variable of the left row and reduces the remaining equality to Q1S1ε1Q1S1ε3. The induction hypothesis gives Q1=Q2S2, hence T=Q2S2S1.

For the guard, suppose it fires while unifying ,Lμ with Mμ. Rewriting reached the shared tail μ, so no label in the finite prefix M is . If a finite substitution T unified the rows, equality of multiplicities would give {}+M(L)+M(Tμ)=M(M)+M(Tμ). Cancel the common multiset M(Tμ). The multiplicity of on the left is 1+M(L)()>0, while it is zero on the right, a contradiction. Thus the guard discards no unifier; (25.10) is its smallest instance. Remaining failures are constructor conflicts, empty-row exposure, or the occurs check, each incompatible with a finite unifier. ◻

Exercise 25.5 — Trace the guard

★★☆ Trace (25.6)(25.7) on (25.10), naming the fresh variable and the substitution returned by rewriting. Identify the exact guard test that fails. Then trace 1μ against 2,1ν and give its MGU.

Exercise 25.6 — Duplicate labels unify

★★☆ Compute an MGU of raise,getμandget,raise,raiseν. State the residual equation and show explicitly how every other unifier factors through yours.

Algorithm W with effects

Algorithm W gains a second variable sort and clauses for operation requests, handlers, and row equations. Variable instantiation, lambda, application, and let-generalization keep their Hindley–Milner form.

Definition 25.15 — Algorithm W with effects

Write Wv(Γ,v)=(S,τ),Wc(Γ,e)=(S,τ,ε). Fresh variables are drawn from one monotonically consumed supply. Thus each choice is distinct from every variable in the environment or term, in the domain or range of a substitution already constructed, and in every type or row returned by an earlier recursive call. A recursive call receives the unused tail of the supply; this state parameter is implicit in the displayed W notation. Substitutions act on the remaining environment before that call. We maintain the invariant that the reported type and row are fixed by the reported substitution; every tuple below is written after applying the substitutions accumulated at that point.

For values:

  1. A variable instantiates every quantified type and row variable in its scheme with a fresh variable and returns the identity substitution.

  2. If C(c)=α¯μ¯.τ, a constant replaces every displayed quantifier by a fresh variable, obtaining Tτ, and returns (id,Tτ).

  3. For λx.e, choose fresh α, compute Wc(Γ,x:α,e)=(S,τ,ε), and return (S,Sαετ).

For computations:

  1. For return v, infer (S,τ) for v, choose a fresh row variable μ, and return (S,τ,μ). A fresh open row is principal because C-Return admits every effect.

  2. For v1v2, compute (S1,τ1)=Wv(Γ,v1),(S2,τ2)=Wv(S1Γ,v2). Choose fresh α,μ.

    Let S3=U(S2τ1,τ2μα). The answer is (S3S2S1,S3α,S3μ). The fresh application-row variable μ is immediately unified with the latent row of the inferred function type; it is therefore absent from later worked traces unless that unification leaves it unconstrained.

  3. For e1 to x.e2, compute (S1,τ1,ε1), then (S2,τ2,ε2) under S1Γ,x:S1τ1. Let S3=U(S2ε1,ε2). Return (S3S2S1,S3τ2,S3ε2).

  4. For perform  v, compute (S1,τ)=Wv(Γ,v), let S2=U(τ,P), and choose fresh μ. Return (S2S1,R,μ). The declaration types P,R are closed, so applying any current substitution to them would be redundant.

  5. For let x=e1 in e2, infer (S1,τ1,ε1), unify ε1 with , and call the result S2. Put χ1=genc(S2S1Γ,S2τ1!)=a¯.(S2τ1!),σ1=a¯.S2τ1 by quantifying variables free in the type-and-effect pair but not the environment. The computation scheme χ1 records the pure effect for the soundness and principality statement; the term environment stores only its value-type projection σ1. Infer (S3,τ2,ε2) for e2 under S2S1Γ,x:σ1, and return (S3S2S1,τ2,ε2).

  6. For handle e with H, use the following accumulator. For an accumulated substitution Q, define the oriented solve step (31.13)R=U(QX,QY),solve(Q;XY)=RQ. where the inferred object is always on the left and the expected object on the right. Choose fresh β,μ, compute (S0,A0,ε0)=Wc(Γ,e), and put Q0=S0. For the return clause, compute (Sr,Br,δr)=Wc(Q0Γ,x:Q0A0,er), then set Qr0=SrQ0,Qr1=solve(Qr0;Brβ),Qr=solve(Qr1;δrμ). Starting with Q0c=Qr, process clauses in the fixed source order. The tempting resumption assumption (bad)ki:RiQi1cε0Qi1cβ uses the handler input row. It is wrong: resuming reinstalls the handler, so a request already removed by the handler would be counted again. For a concrete counterexample, extend the operation signature by Σ(ask)=1Env. For a one-clause ask handler with input askμ and output μ, the bad type gives ki() an extra ask; the clause can no longer satisfy its required output μ. The resumption therefore has type RiQi1cμQi1cβ, using the accumulated output row. At clause i, compute (Si,Bi,δi)=Wc(Qi1cΓ,pi:Pi,ki:RiQi1cμQi1cβ ; ei). Again Pi and Ri are closed declarations; only the inferred output type and row require the accumulated substitution. The semicolon separates the extended context from the clause term. Put Qi0=SiQi1c,Qi1=solve(Qi0;Biβ),Qic=solve(Qi1;δiμ). Finally set (31.14)Q=solve(Qnc;ε01,,nμ). Return (Q,Qβ,Qμ).

Every recursive call is on a proper subterm, and every equation goes to the terminating unifier. The fixed source order is part of this algorithm. We do not claim a separate clause-order-independence theorem; principality is proved for this stated order below.

Lemma 25.16 — Computation and pure-let generalization

Let Γe:A!ε, and put χ=genc(Γ,A!ε). Every instance of the pair A!ε that leaves Γ fixed is an instance of χ, and χ is most general with this property. If ε=, deleting the fixed empty effect from χ gives genv(Γ,A), the most-general value scheme admitted by C-Let.

Proof of Lemma 25.16 — Computation and pure-let generalization

Proof. Partition the free variables of A!ε into those free in Γ and the remainder. A substitution that leaves Γ fixed cannot quantify or rename the first part; its action on the remainder is precisely an instantiation of the variables quantified by genc. The identity instantiation recovers the pair, and any other scheme whose instances are exactly the Γ-fixing instances must quantify only variables in the same remainder. Its instances therefore factor through χ. When the row is empty, it contributes no free row variables, so the quantified list is exactly the one in genv(Γ,A). Purity permits the returned value to be used repeatedly without duplicating an operation. ◻

Theorem 25.17 — Soundness of inference

If Wv(Γ,v)=(S,A), then SΓvv:A. If Wc(Γ,e)=(S,A,ε), then SΓe:A!ε.

Proof of Theorem 25.17 — Soundness of inference

Proof. Proceed by simultaneous induction on values and computations. A variable is typed by V-Var at the fresh instance selected by W; a constant is typed by V-Const at the corresponding fresh instance of its closed declared scheme. For a lambda, the computation induction hypothesis types the body under SΓ,x:Sα, so V-Lam gives precisely W’s arrow.

At every sequential inference stage, maintain this invariant: after the accumulator is Qj, lemma 31.8 transports every judgment inferred through stage j to context QjΓ and applies Qj to its reported type and row. Each new MGU is composed only after this transport.

For a return, the value hypothesis derives SΓvv:A, and C-Return permits W’s fresh μ. In an application, lemma 31.8 transports both value hypotheses through the substitutions returned by the later calls, so they type the operator and operand after the accumulated substitution. Unifier soundness changes the operator type to τ2μα; C-App gives the reported result and effect. The operation case is the same calculation against the fixed parameter type P, followed by C-Op. For sequencing, the two computation hypotheses type the premises at S2ε1 and ε2; unifier soundness proves S2ε1ε2, and C-To gives W’s substituted result.

For let, the accumulator invariant transports the bound judgment before unification establishes the empty effect required by C-Let. Then lemma 25.16 justifies the environment scheme, and the induction hypothesis types the body. For a handler, the invariant transports the body, return, and each operation-clause judgment in the fixed clause order. The sequential unifications establish exactly (25.14), a return clause of result β!μ, and operation clauses of that result under pi:Pi and ki:Riμβ. These are the premises of C-Handle. ◻

Lemma 31.21 — Non-handler factorization step

Consider a declarative value or computation derivation whose last rule is not C-Handle. Suppose that the principal-factorization conclusion holds for each immediate typing premise of that last rule. Then W succeeds on the conclusion. For a computation conclusion TΓe:A!ε, if Wc(Γ,e)=(S,A0,ε0), there is a substitution Q such that (31.15)TΓQSΓ,AQA0,εQε0. For a value conclusion TΓvv:A, Wv(Γ,v)=(S,A0) succeeds, and there is a substitution Q such that TΓQSΓ,AQA0.

Proof of Lemma 31.21 — Non-handler factorization step

Proof. Inspect the last rule, using the assumed factorization for each immediate typing premise.

Ordinary forms. The value and computation cases use the common factorization statement (25.15). A declarative variable instance factors through W’s fresh instance by mapping each fresh variable to the type chosen in the derivation. A declarative constant instance factors in the same way through W’s fresh instance of its closed declaration. In the lambda case, inversion gives a monomorphic parameter type and the corresponding body derivation. Map W’s fresh parameter variable to that type and apply the assumed computation factorization to the body. In the return case, apply the assumed value factorization and map W’s fresh μ to the derivation’s chosen effect row.

Application, operation, and sequencing. For application, inversion yields types A and B such that the operator has type AεB and the argument has type A. The two value hypotheses first factor their inferences; the residual declarative substitution then unifies W’s generated arrow equation. MGU factorization produces the next residual Q. For an operation, the assumed value factorization handles the parameter inference; the residual declarative substitution then unifies W’s equation with P, after which the declarative tail is the image of W’s fresh μ. For sequencing, the two computation hypotheses factor the subterms from left to right; inversion says their effects are equivalent, so the residual substitution solves W’s effect equation. The row MGU theorem factors it once more. These compositions give all three equations in (25.15).

Pure let. In the let case, inversion gives an empty effect. The assumed factorization followed by MGU factorization reaches W’s empty-row unifier. Lemma 25.16 factors the scheme used by the declarative body through W’s generalized scheme. Apply the assumed body factorization and compose.

Row conversion. If the last rule is C-RowConv, write its premise effect as ε, so εε. The assumed premise factorization premise factors the same syntax-directed W run as Wc(Γ,e)=(S,A0,ε0), with residual Q such that TΓQSΓ, AQA0, and εQε0. Symmetry and transitivity give εQε0, so the same Q proves (25.15) for the converted conclusion; W needs no additional equation or solve step. ◻

Lemma 31.22 — Handler-accumulator factorization step

Suppose the last rule is C-Handle, and suppose the principal-factorization conclusion holds for its body, return clause, and operation clauses. Then the handler run of Wc succeeds and produces (S,A0,ε0) with a residual Q satisfying (25.15).

Proof of Lemma 31.22 — Handler-accumulator factorization step

Proof. For a handler, invert C-Handle and follow the accumulator literally. Before following its premises, extend the declarative substitution T to each fresh W variable by the type or row selected by the inverted declarative derivation. The fresh variables lie outside the original domain, so the two parts of the extension are disjoint. After accumulator stage j, maintain a residual substitution Θj with (31.16)T=ΘjQjon the environment variables and every fresh variableintroduced through stage j. The assumed body factorization initializes this invariant at Q0=S0. For the return clause, its assumed factorization handles the declarative clause derivation through SrQ0. Inversion says that the declarative return type and effect are the handler’s β and μ. The residual sends the fresh reported type variable to the inverted declarative result and therefore unifies the first oriented equation Qr0BrQr0β. After that MGU is composed, the residual sends the fresh row variable to the inverted declarative effect and unifies the second equation Qr1δrQr1μ. The MGU factorization theorem factors it through Qr1 and then Qr, establishing (25.16) after the return stage.

Suppose (25.16) holds before clause i. Its assumed factorization handles the declarative clause derivation through SiQi1c. The inverted clause premise gives the expected type Tβ, effect Tμ, and continuation type TRiTμTβ. Thus the residual solves the reported-type equation and then the reported-effect equation. Two MGU factorizations yield Qi1 and Qic, preserving the invariant. Induction over the fixed clause order reaches Qnc. Finally, the inverted handler-input premise solves the row equation in (25.14); one last MGU factorization yields Q and its residual. This is the required factor Q in (25.15). ◻

Theorem 25.18 — Completeness and principal factorization

Suppose TΓe:A!ε. Then Wc(Γ,e)=(S,A0,ε0) succeeds, and there is a substitution Q satisfying (25.15). If TΓvv:A, then Wv(Γ,v)=(S,A0) succeeds, and there is a substitution Q such that TΓQSΓ,AQA0. Thus, for an empty environment, genc(,A0!ε0) is the principal computation scheme.

Proof of Theorem 25.18 — Completeness and principal factorization

Proof. Proceed by simultaneous induction on value and computation derivations. At every last rule other than C-Handle, apply lemma 31.21 to the induction hypotheses for its immediate typing premises. At C-Handle, apply lemma 31.22 to the body, return, and clause induction hypotheses. These cases exhaust the declarative rules and establish both factorization statements. For Γ=, lemma 25.16 turns the computation factorization into the displayed principal scheme. ◻

Here is the complete row part of W on the transaction. Name the fresh tail of the final return ρr, the tail of put by ρp, the instantiated callback tail by ρf, and the tail of get by ρg. Working from the final sequence outward, W generates the following equations and MGUs: equationnew bindingsputρpρrρrputρpraiseρfputρpρpraiseξ,ρfputξgetρgput,raiseξξgetμ,ρgput,raiseμ. For the second equation, exposure skips put, expands ρp with raise, and leaves residual putξ. For the third it skips put,raise, expands ξ with get, and leaves residual put,raiseμ. Thus the composite substitution is ρrput,raise,getμ,ρpraise,getμ,ρfput,getμ,ξgetμ,ρgput,raiseμ. The callback instance is therefore ν=ρf=get,putμ, modulo exchange, and every sequencing node has common effect Eμ. Since μftv(Γf), computation generalization gives genc(Γf,Int!Eμ)=μ.(Int!Eμ), as in (25.2). For the monomorphic value λf.tf, W instead gives f:1εfInt a fresh monotype. Its row equations are equationnew bindingsputρpρrρrputρpεfputρpεfputρpgetρgputρpρpgetμ,ρgputμ. Thus the common row is Gμ, and V-Lam gives (25.3). This is the promised full trace; no handler-specific heuristic occurs in it.

We can now compare four handlers at one formal signature. Write ΓH:A!εinB!εout as an abbreviation for the return- and operation-clause premises of C-Handle, with those input and output rows. Combining those premises with Γe:A!εin in C-Handle gives Γhandle e with H:B!εout.

The fallback handler already calculated above satisfies d:B CdB:B!raiseμB!μ. For a reader environment r:Env, define Haskr={return xreturn x;ask(u;k)kr}. The two premises are x:Areturn x:A!μ,u:1, k:EnvμAkr:A!μ, so r:EnvHaskr:A!askμA!μ.

Here is the one-clause handler accumulator once in full. For hr=handle (perform ask ()) with Haskr, the body reports Env!askμ0. Choose output variables β,μ. The return clause reports Env!δr, so its two solve steps bind βEnv and δrμ. The operation context is then constructed as u:1,k:EnvμEnv,r:Env. Its body kr already reports Env!μ, so the clause’s two solve steps are identities. The final input equation askμ0askμ binds μ0μ. Thus W returns Env!μ, with relative scheme μ.(Env!μ). The continuation’s output row is chosen when its context entry is constructed; it is not discovered by a later equation.

For a second comparison, add the operation Σ(choose)=1Bool. The next handler deliberately resumes twice: Htwice={return xreturn x;choose(u;k)ktrue to x.kfalse}. Here x:1 is intentionally unused. Both applications of k:Boolμ1 have result 1!μ; C-To therefore derives the clause, while the return clause is x:1return x:1!μ. Hence Htwice:1!chooseμ1!μ. Although k occurs twice, C-To derives the clause because it places no affine-use premise on the continuation variable.

Finally define a state-passing handler for unit-returning computations: Hst={return xreturn (λs.return s);get(u;k)return (λs.ks to g.gs);put(s;k)return (λs.k() to g.gs)}. Put D=IntμInt. The return clause has type D!μ. In the get clause, k:IntμD; both ks and gs have effect μ, so the lambda has type D. In the put clause, k:1μD; the same derivation ends in gs. Thus Hst:1!get,putμD!μ. The exception, reader, choice, and state examples all use the same final row equation in W. Only their clause terms differ.

Exercise 25.7 — W on the transaction

★★★ Insert perform log e to w after the put in tf, with e:Error, and run W on the monomorphic wrapper. Name the fresh row variables and show that the callback and body share the principal row get,put,logμ. Then instantiate its tail with one raise and compare that instance with the separate let-polymorphic callback trace above.

Exercise 25.8 — W on a deep handler

★★☆ In the context r:Env, run W on the fully specified computation hr=handle (perform ask ()) with Haskr. Name the fresh variables and the accumulator substitutions after the body, return clause, operation clause, and input-row equation; then give the principal computation scheme relative to r:Env. Point to the accumulator extension where the continuation is constructed with the output row μ, and explain why using the handler’s input row there would be unsound.

The duplicate-free bridge to set-indexed effects

Row polymorphism here is ordinary kinded quantification. A scheme may quantify μ, and duplicate labels make extension and elimination unconstrained. There are no type classes, qualified types, subeffect inequalities, or predicates μ. Adding any of those changes both principal schemes and the solver theorem. The MGU theorem is not a theorem about unique-label rows with hidden lacks constraints.

The set-indexed calculus forgets multiplicity. For any closed row, write ε for its finite support: the set of labels occurring at least once. The target signature is Σ()=PR. Translate by (31.17)AεB=U(AεFB),λx.e=thunk(λx.e),vw=(force v)w,return v=return v,e1 to x.e2=e1 to x.e2,perform  v=v(x.return x),handle e with H=handleewithH. Variables and base constants map to themselves, and handlers map clause by clause: H={return xer; i(pi;ki)ei}i=1n. Contexts map pointwise: Γ,x:A=Γ,x:A. Within a translated clause, a source call kiv is (force ki)v, because the CBPV handler binds its continuation as a thunk. A computation type A!ε maps to FA!ε.

The target handler rule of chapter 22 checks each continuation at the chosen output set Eout, requires EinHEout, and admits ordinary effect weakening. The translation proof establishes precisely these continuation, residual- inclusion, and weakening premises; it does not require equality of the two effect sets.

Theorem 25.19 — Closed-row support translation

Call a derivation monomorphic when every variable assumption has a scheme with no quantified type or row variables. Require every constant instantiation to be fixed before the derivation begins. If such a derivation contains no C-Let root and uses only closed rows, then the judgment Γe:A!ε yields Γce:FA!ε in the set-indexed CBPV core. A source value judgment Γvv:A yields the value judgment Γvv:A.

Proof of Theorem 25.19 — Closed-row support translation

Proof. Induct on typing. A source return first uses the pure CBPV return rule and then effect weakening; an atomic operation uses an explicit pure return continuation and then weakening to the translated tail. Sequencing uses EE=E. Lambda, force/application, and the remaining value rules are the corresponding CBPV rules after type translation. The let-free hypothesis removes the only rule whose target would require polymorphic schemes, which the chapter 22 core does not contain. A C-RowConv step translates to the identical target annotation because row exchange does not change the underlying finite set.

It remains to check the displayed handler clause. Invert C-Handle and write H={1,,n},E=ε,Ein=HE. The body induction hypothesis gives Γce:FA!Ein, and the return-clause hypothesis gives er:FB!E under x:A. For operation clause i, type translation maps the source resumption assumption ki:RiεB exactly to pi:Pi,ki:U(RiEFB). The clause induction hypothesis therefore types ei at FB!E under the target parameter and resumption binders; its calls use (force ki)v, as specified above. For arbitrary multiplicities, set algebra gives EinH=(HE)HE. This inclusion discharges the residual-effect premise of target C-Handle; the return and operation-clause induction hypotheses discharge its remaining premises. Hence the translated handler has type FB!E. ◻

For a concrete covered term, fix e0:Error. The closed, duplicate-free judgment perform log e0:1!log translates to loge0(x.returnx), typed in the set core at F1!{log}. A closed-row fallback handler gives the corresponding one-clause handler example.

The target calculus makes an operation’s continuation part of the operation node, whereas the row calculus first exposes an atomic request and lets an evaluation context supply its continuation. Its target also has administrative force/thunk steps and algebraic reassociation of sequencing. A simulation theorem would therefore need an explicit target closure modulo those equations and a proof that every source context maps to that closure. The theorem is a typing bridge rather than a simulation. Its information loss is visible on the rethrower: the target handler premise is an inclusion, so erasing both input and output to a set containing raise still typechecks. What erasure cannot express is which occurrence was discharged. On duplicate-free closed rows, support is injective up to row exchange; on rows with multiplicity, distinct source annotations collapse. Consequently support erasure is not faithful on general rows.

Generalized evidence passing

The inference calculus above and the following compilation calculus share scoped rows, but they have different endpoints. Freeze Xie and Leijen’s explicitly typed System Fε source with σ::=ακcκσ¯σ1εσ2ακ.σ,ε::=εαeff. The prompt/evidence intermediate Fpw adds internal terms prompt m h e and yield m v. Evidence and evidence vectors have the exact forms q::=(m,h,w),w::=:qw. Selection must find the most recently extended evidence: :qw.=q,:qw.=w.(). The third component stores the evidence context in which h was defined. The Fpw fragment frozen here retains that source representation but does not inspect the selected tail w in its displayed perform step. The source’s later tail-resumptive optimization gives the component operational work through an under frame; that optimization and its rules are outside this card.

On a performed operation, evidence selection replaces dynamic search: wperform op ε0 σ¯ v0yield m(λεk.fσ¯vk) when w.=(m,h,w), the clause (opf) occurs in h, and the global signature assigns op to . A prompt extends the current evidence vector by :(m,h,w)w while evaluating its body. Thus the selected evidence and the nearest dynamic prompt agree only under a reachability invariant.

Definition 31.25 — Internal safety

An Fpw expression e is internal-safe when there is a closed, well-typed expression e0, containing no prompt, yield, or marker, such that e0e.

This is Definition 1 of the source read as its least fixed point, rather than as a circular closure clause. It ensures that each prompt owns a unique marker generated by the handler rule and that every yielded marker came from type-correct evidence selection. Arbitrary user-written prompt or yield terms are outside the definition.

Theorem 31.26 — Internal-safe preservation

If e:σ, e is internal-safe, and ee, then e:σ.

Proof of Theorem 31.26 — Internal-safe preservation

Source import. This is Theorem 2, §3.1.4, pp. 18–19 of [XL21]. Its induction uses the clause-typing and generated-marker invariants of definition 31.25. ◻

Theorem 31.27 — Internal-safe progress

If e:σ and e is internal-safe, then e is a value or there are w,e with wee.

Proof of Theorem 31.27 — Internal-safe progress

Source import. This is Theorem 3 at the same source location. Source Theorem 1, immediately before Definition 1 on p. 19, relates evidence-indexed reduction to the evidence extracted from the evaluation context and supplies the selected handler in the request case. ◻

Theorem 31.28 — Live-marker uniqueness

If an internal-safe expression contains prompt m1 h1(E2[prompt m2 h2 e]), then m1m2.

Proof of Theorem 31.28 — Live-marker uniqueness

Source import. This is Theorem 4, §3.1.4, p. 19 of [XL21]. Both markers arise from reachable handler-generated prompts; the theorem’s distinct-marker invariant therefore applies to the displayed nesting. ◻

The next intermediate Fpb propagates a yield m f k term outward through evaluation contexts, so the monadic translation can realize it as the Yield constructor of the target control monad. The target is a plain higher-kinded polymorphic lambda calculus with Mon ε A:=Evv εCtl ε A, where a translated computation accepts its evidence vector explicitly. Translation is type directed: Γe:σεe. Values become functions returning Pure; application sequences the two translated computations by monadic bind, e1e2gep=e1gep(λf.e2gepf); a performed operation uses the evidence selector for its label, for example vgep=λw:Evv ε.Pure ε σgep vgep,v,perform opgep=perform(select op). The complete target and representative translation rules are frozen in subappendix A.28.

Theorem 31.29 — Generalized-evidence operational endpoint

Let e be a closed System-Fε program and let e:Inte. If en, then ePure  Int n. If e diverges, then e diverges.

Proof of Theorem 31.29 — Generalized-evidence operational endpoint

Source import. This is Theorem 7, §4.4, p. 25 of the version-4 report [XL21]. Its proof composes the source to multi-prompt simulation (Theorem 14), evidence-passing simulation (Theorem 15), bubbling simulation (Theorem 18), and monadic simulation (Theorem 19). The statement requires a closed integer source, empty source row, empty initial evidence vector, and the final Pure result. It states neither arbitrary-open-program correctness nor correctness of generated C. ◻

The pinned MpEff release implements insertion-ordered generalized evidence vectors as a Haskell library. It does not contain the mechanized proofs and is not the Koka-to-C compiler. Current Koka uses canonical evidence and further optimizations. Results from either implementation are artifact evidence, not new hypotheses for theorem 25.18.

Exercise 31.9 — Selection and marker provenance

★★☆ Build two nested evidence entries for the same label , with distinct markers mo and mi. Calculate selection of the newest entry and the resulting yield term. Then explain which definition 31.25 invariant rules out reusing mo for the inner prompt. State separately why the displayed Fpw perform step does not inspect the selected entry’s third component.

Effect exclusion is a separate calculus

An open duplicate row μ states that is present. It cannot state that is absent from the unknown tail μ. For example, a recovery combinator may require its handler argument to perform any effects except raise. Neither μ nor raiseμ expresses that negative premise.

The calculus λ solves a different problem. Fix a finite closed universe U of effects and take Boolean formulas φ::=β{F}φcφφφφ,φψ:=φψc. Formulas are identified by equality under every valuation, φBψ. Function types carry latent formulas. There is no subeffecting rule.

The term e without F pushes a stack frame forbidding F. To keep the source’s T-prefixed rules distinct from the adjacent System-Xi card, this book uses the prefix Ex. Its typing rule is Γe:τφφ{F}BΓe without F:τφExWithout. At runtime, do F(v) steps only when Fforb(k). The local machine interface is WF:=[ ] without F,Lx,e:=let x=[ ] in e,k::=WF::kLx,e::k,forb()=,forb(WF::k)={F}forb(k),forb(Lx,e::k)=forb(k). A configuration is ek. The machine judgment is mek ok; the stack judgment τkkφ records the formula forbidden by k. Here is the source’s judgment separator, not the Boolean difference operation in φψ. The exact machine and stack rules in subappendix A.28 maintain disjointness between performed and forbidden effects.

Theorem 31.30 — Effect exclusion safety

If mdo F(v)k ok, then Fforb(k).

Proof of Theorem 31.30 — Effect exclusion safety

Source import. This is Theorem 3.14, §3.4, p. 16 of Lutze et al. [LMSB23]. Inversion of machine typing gives a performed formula φ1, a forbidden formula φ2, and φ1φ2B. Lemma 3.12 identifies φ1 with {F}; Lemma 3.13 identifies φ2 with forb(k). ◻

Theorem 31.31 — Effect-exclusion machine safety

A well-typed machine is final or takes a step, and machine typing is preserved by every step.

Proof of Theorem 31.31 — Effect-exclusion machine safety

Source import. Progress and preservation are Theorems 3.10–3.11, §3.4, p. 15 of [LMSB23]. The progress proof uses theorem 31.30 in its T-Do case. Preservation is a separate case analysis on the machine step and does not depend on effect exclusion safety. ◻

This theorem belongs to the fixed-universe Boolean row theory. Its principal types are principal only modulo Boolean equivalence, and its inference uses Boolean unification. None of these facts follows from the duplicate-label MGU of theorem 25.14; conversely, the exclusion paper and its VM artifact prove no theorem about the inference calculus of this chapter.

Exercise 31.10 — A negative row witness

★★☆ Give a type for a recovery combinator whose handler may perform effects β{raise}. Derive its Ex-Without premise, then show why neither an open row variable μ nor one positive extension of μ entails the required absence statement.

The row choice and exposure unifier follow Leijen’s scoped-label development [Lei05]. Koka uses duplicate labels and a closely related inference algorithm [Lei14]. It uses duplicate effect labels for the same reason and proves soundness and principality for its own syntax-directed system; the deep-handler calculus and set-erasure theorem above are proved locally.

Exercise 25.9 — The multiplicity boundary

★★☆ Apply set erasure to the pure and rethrowing catches. Show which two distinct rows raise,raiseν and raiseν collapse to one set. Show that the rethrower’s erased target handler nevertheless satisfies the target premise EinHEout. Compare this with a fallback whose output contains no raise, and explain exactly which multiplicity information is lost even though typing is preserved.

Exercise 25.10 — Qualified alternative

★★☆ Redesign row extension so labels are unique. State the lacks constraint needed to extend μ by . Rewrite the one-clause instance of C-Handle and W’s final input-row equation with that premise. List the soundness, completeness, constraint-entailment, and factorization properties needed before a principality theorem could be claimed.

Suggested first pass.

Begin with exercise 25.11, continue with exercise 25.12, and finish with exercise 25.13.

Exercise 25.11

★★☆ Prove the factorization half of lemma 25.12 when the visible prefix has two equal skipped labels. Display the exchanges, cancellation, and residual factor; identify the step that needs multiplicity.

Exercise 25.12

★★★ Give the complete one-operation handler case of Theorem 25.18. Give the residual after the body, return clause, operation clause, and input-row equation, then pair every equation with its rule, MGU, and factor.

Exercise 25.13

★★☆ Nest a handler that forwards raise inside one that handles it. Derive the types of both reduction roots, then use multiplicities and Theorem 25.10 to exclude an exposed request when the final effect is empty.

Exercise 25.14 — (*)

★★★ Replace duplicate labels by unique rows plus lacks constraints. State a well-kinded exposure judgment with its lacks premise, its factorization theorem, and the one-operation handler case of W. Compare these proof obligations with those of (25.11).

Exercise 25.15

★★★ Practical project.effect-row-inferencer Implement the finite row and handler fragment in Kappa. Represent open rows with duplicate-preserving prefixes; implement exchange comparison, single-occurrence cancellation, guarded exposure, and the one-layer handler transition. The permanent corpus must distinguish exchange from contraction, remove exactly one duplicate, compute one open-row MGU, reject the shared-tail cycle by the guard rather than fuel exhaustion, retain get, put, and raise in the transaction trace, forward once to the nearest handler, leave one raise for an outer fallback, and construct the reader resumption with the handler output tail. The oracle is the ordered list of these eight results. As a semantic mutation, recurse on the unmodified right row in prefix comparison; it must still type-check and audit cleanly but fail the oracle. Appendix E records the four acceptance commands, and appendix F gives the implementation stages.

Search the book

Type to search the local edition.