Lectures onType Theory
Chapter 28
Chapter 28Core route

Effects, Monads, CBPV, and Algebraic Operations

The equation (λx.())M=() is harmless when M is a terminating pure term. It is false as an equation of programs as soon as evaluating M can raise an exception or change a cell. Under call by value, M runs before the function is entered; under call by name, the unused argument does not run at all. Evaluation order has become observable.

A computation with effects must record more than its final result. A well-founded operation tree has internal nodes that request operations and leaves that return values. Sequencing such trees will force the neutral-return and associative-sequencing equations rather than assume them.

Computations are not merely results

Fix a set S of stores and a set Exc of exceptions, and write 1={} for a singleton set and 0= for the empty set. Consider three operation symbols get:1S,put:S1,raise:Exc0. The set to the left of is the parameter sent to the outside world. The set on the right is the response returned to the continuation. Thus a read sends no information and receives a store; a write sends a new store and receives unit; an exception receives no response at all. This signature arrow is static.

Let e0:Exc and let s+1 denote a fixed update of s:S. Define transaction=get() to s.put(s+1) to _.raise(e0) to z.returnz. The notation M to x.N is a bind: it runs the request or computation M, names its returned value x, and continues as N. The last continuation is unreachable because z:0. Yet the preceding write is not thereby erased. Whether it remains visible depends on the order in which state and exception requests are interpreted.

The pure result type alone records none of this. Assigning the transaction an arbitrary result type A says what a successful return would contain; it does not say that success is impossible, that a write occurs first, or that an enclosing interpreter may roll the write back. We need a mathematical object that retains both return leaves and operation requests.

Exercise 22.1

★☆☆ Let tick increment an observable counter and return unit. Compare the call-by-value and call-by-name evaluations of (λx.())tick(). Identify the exact step at which the two traces differ.

Exercise 22.2

★☆☆ Explain why the response type of raise is 0, rather than 1. What illusory execution path would a continuation of type 1A suggest?

The free operation tree

A finite operation signature Σ assigns to every operation symbol op a parameter set Pop and a response set Rop. We write Σ(op)=PopRop. No equations between operations are assumed yet.

Definition 22.1 — Well-founded operation trees

For a set A, the set TΣA is generated by t::=Ret(a)Opop(p,k), where a:A, p:Pop, and k:RopTΣA. Trees are well founded, though an operation may have infinitely many immediate branches when its response set is infinite. Equality of operation nodes is pointwise in k.

For an operation request, write the smart constructor op(p):=Opop(p,Ret). Thus op(p) to x.N expands to Opop(p,λx.N): an operation node whose response selects the continuation branch. The transaction in the opening display is the corresponding surface notation for the tree expanded below.

If ΣΣ preserves the parameter and response sets of every old operation, define TΣATΣA by Ret(a)Ret(a) and Opop(p,k)Opop(p,λr.ι(k(r))).

The associated induction and recursion principles quantify over every response branch, even when there are infinitely many. To prove P(t) for all t:TΣA, it is enough to prove a:A. P(Ret(a)),op,p,k.(r:Rop. P(k(r)))P(Opop(p,k)). The recursion principle has the same premises, with recursively computed values supplied for every k(r). These principles define the least set closed under the two constructors: infinite branching changes the number of induction hypotheses at a node, not the well-foundedness of its branches.

The response-indexed family k is the rest of the computation. For example, the transaction is the tree Opget((),λs.Opput(s+1,λ_.Opraise(e0,absurd0))), where absurd0:0TΣA is the unique empty function. This expression is well typed for every A, but it has no Ret leaf.

Sequencing must replace each successful return leaf of the first computation by the second computation, while retaining every pending operation.

Definition 22.2 — Return and bind

For a:A, t:TΣA, and f:ATΣB, define returna:=Ret(a),Ret(a)=f:=f(a),Opop(p,k)=f:=Opop(p,λr.k(r)=f). The last line is recursion on the tree, not an equation assumed about an unspecified effect.

Definition 22.3 — Monad, in return-and-bind form

A monad in this chapter consists of a type constructor T, maps returnA:ATA,(=):(TA)(ATB)TB, and the left-unit, right-unit, and associativity equations displayed in lemma 22.4. The word thus names a uniform sequencing interface whose laws permit insertion or reassociation of sequencing without changing a computation.

Lemma 22.4 — Monad laws forced by sequencing

For t:TΣA, f:ATΣB, and g:BTΣC, returna=f=f(a),t=return=t,(t=f)=g=t=(λa.f(a)=g). The first equality is definitional. The other two are propositions about trees.

Proof of Lemma 22.4 — Monad laws forced by sequencing

Proof. The left-unit equation is the first clause for bind. For right unit, induct on t. At a return leaf, Ret(a)=return=Ret(a). At an operation node, the induction hypothesis gives k(r)=return=k(r) for every response r, hence Op(p,k)=return=bindatanoperationOp(p,λr.k(r)=return)=inductionhypothesisOp(p,λr.k(r))=metaleveletaOp(p,k). The final equality is the meta-level eta equation λr.k(r)=k; pointwise equality of operation continuations then transports it through Op.

For associativity, again induct on t. The return case is (Ret(a)=f)=g=f(a)=g=Ret(a)=(λx.f(x)=g). For an operation node, calculate every line: (Op(p,k)=f)=g=bindatanoperationOp(p,λr.k(r)=f)=g=bindatanoperationOp(p,λr.(k(r)=f)=g)=inductionhypothesisOp(p,λr.k(r)=(λa.f(a)=g))=bindatanoperationOp(p,k)=(λa.f(a)=g). The induction-hypothesis label is pointwise in every response r. ◻

The preceding calculation proves the monad laws for TΣ. For comparison, define Maybe(A)=A+1 with return(a)=some(a),none=f=none,some(a)=f=f(a). Put h(a)=f(a)=g; then return(a)=f=definitionofreturnsome(a)=f=bindatsomef(a),none=return=bindatnonenone,some(a)=return=bindatsomereturn(a)=definitionofreturnsome(a),(none=f)=g=bindatnonenone=g=bindatnonenone,none=h=bindatnonenone,(some(a)=f)=g=bindatsomef(a)=g=definitionofhh(a),some(a)=h=bindatsomeh(a). The last four rows prove associativity because the two sides reduce to the same result in each case. Nothing categorical is required for either calculation.

Proposition 22.5 — Algebraicity of a requested operation

For every operation node and every continuation f, Opop(p,k)=f=Opop(p,λr.k(r)=f). Thus an operation commutes with all subsequent sequencing: the sequencing is pushed uniformly into every response branch.

Proof of Proposition 22.5 — Algebraicity of a requested operation

Proof. This is the operation clause of definition 22.2. Its significance is that the equation is uniform in the result type and in f; it is not a special property checked separately for reads, writes, or exceptions. ◻

Definition 22.6 — Effect theory and Kleisli congruence

The raw tree validates only equations forced by its constructors. We write a generating equation in a three-sorted context as ΞsT=tT:TΣA. The context Ξ may contain ordinary value variables x:B, computation variables m:TΣB, and continuation variables k:BTΣC. The sides sT,tT are well-sorted tree expressions generated from those variables by Ret(v), operation formation Opop(p,λq.sT), sequencing sT=λx.tT, and continuation application k(v). Here v,p are ordinary well-sorted set-level expressions, and the displayed lambdas bind their indicated value variables.

An instantiation assigns set elements to the ordinary variables, trees of the declared result type to computation variables, and functions from responses to trees to continuation variables. It extends homomorphically through the displayed constructors and sequencing. Thus an instantiated equation has two actual members of the same TΣA; no untyped syntactic substitution is implicit.

An effect theory is a declared collection of such equations. Besides the ordinary instantiations just described, every declaration is read under arbitrary Kleisli substitution: if an instance has result set A, then for every f:ATΣB its two sides may both be sequenced with f. Its congruence is the least equivalence containing all these instances and closed under every Op constructor and pointwise replacement of continuation branches. Equivalently, it is closed under sequencing: if tt, then t=ft=f, and pointwise equivalent continuations may replace one another. The Kleisli-instance clause is essential.

For a concrete failure, suppose a generator merely identifies two closed leaves Ret(false)Ret(true):TΣBool. Constructor congruence alone preserves that equation, but take f(false)=Ret(0) and f(true)=Ret(1). The two representatives bind to Ret(false)=f=Ret(0)andRet(true)=f=Ret(1), which that generator-only congruence need not identify. Bind is therefore not well-defined until the equation is closed under every Kleisli substitution. Familiar state equations such as reading twice being equivalent to reusing the first answer do not follow. To impose an effect theory T, one must quotient trees by the least congruence containing its declared equations. A fold through that quotient must equalize every generating equation under every tree-valued instantiation of its computation and continuation variables and under every Kleisli substitution. Here a carrier is simply the fold’s target set, and its Σ-algebra is the collection of functions chosen to interpret operation nodes; the return map is a separate assignment of the free generators. Definition 22.7 states these data formally. The stronger condition that the whole carrier algebra models T is sufficient, but is not necessary when the fold does not reach the whole carrier. Three claims are therefore separate: the monad laws follow from sequencing, algebraicity follows from the operation constructor, and state-specific equations require an explicit state theory.

For reference, a standard global-state theory contains the following four schemata, among equivalent presentations, for all appropriately typed continuations: get() to s.get() to s.k(s,s)=get() to s.k(s,s),get() to s.put(s) to _.m=m,put(s) to _.get() to s.k(s)=put(s) to _.k(s),put(s) to _.put(s) to _.m=put(s) to _.m. These are equations of trees, not extra reduction rules. In particular, the first equation is the specific “read twice” law used in exercise 22.4.

Exercise 22.3

★☆☆ Repeat the operation-node case of associativity without suppressing the operation subscript or the response type. Mark where the built-in pointwise equality of continuation branches is used; this is the extensional principle already stipulated in definition 22.1, not an additional appeal to function extensionality.

Exercise 22.4

★☆☆ Construct two distinct raw trees which a usual state theory would identify. Explain why constructor injectivity prevents their equality in TΣA.

Handlers are folds

To interpret a tree, choose what to do with returns and with every operation node. The continuation has already been recursively interpreted by the time the operation clause receives it.

Definition 22.7 — Handler algebra and fold

A Σ-algebra consists of a carrier C and, for every operation, a map hop:Pop×(RopC)C. Given a generator assignment η:AC, its fold is the map defined by foldη,h(Ret(a))=η(a),foldη,h(Opop(p,k))=hop(p,λq.foldη,h(k(q))).

Theorem 22.8 — Existence and uniqueness of handling

The displayed fold exists. Moreover, if q:TΣAC satisfies the same return and operation equations, then q=foldη,h.

Proof of Theorem 22.8 — Existence and uniqueness of handling

Proof. Existence is structural recursion on the well-founded tree. For uniqueness, prove q(t)=foldη,h(t) by induction on t. At a return leaf, both sides are η(a). At an operation node, the defining equation for q and the induction hypothesis at every response u give q(Op(p,k))=definingequationforqhop(p,λu.q(k(u)))=pointwiseinductionhypothesishop(p,λu.foldη,h(k(u)))=definitionoffoldfoldη,h(Op(p,k)). ◻

This is the universal property of the free Σ-algebra on A: Ret inserts the generators, and a map out of TΣA is uniquely determined by the generator assignment η and the operation interpretations h.

An algebra may ignore a continuation, call it once, or call it several times. Exception handling ignores the impossible continuation of raise. A nondeterminism handler can invoke both branches. What algebraicity forbids is inspecting a continuation as syntax or capturing a larger evaluation context that was not supplied as the response function.

By its second defining clause, every fold is a Σ-algebra homomorphism extending η: foldη,h(Op(p,k))=hop(p,λq.foldη,h(k(q))).

Lemma 28.9 — Fold after tree sequencing

For every t:TΣA and f:ATΣB, foldη,h(t=f)=foldfoldη,hf,h(t). The fold on the left has generator map η:BC; the fold on the right has generator map afoldη,h(f(a)).

Proof of Lemma 28.9 — Fold after tree sequencing

Proof. Induct using the tree induction principle following definition 22.1. At a return, both sides are foldη,h(f(a)). At an operation node, foldη,h(Op(p,k)=f)=hop(p,λr.foldη,h(k(r)=f))=hop(p,λr.foldfoldη,hf,h(k(r)))=foldfoldη,hf,h(Op(p,k)), where the middle equality is the pointwise induction hypothesis. ◻

Corollary 22.9 — Quotient descent

If an effect theory T is imposed, this fold descends to the quotient exactly when it gives equal results on the two sides of every generating equation under every well-typed assignment of ordinary values to the value variables and trees to the computation and continuation variables, including every Kleisli instance specified above. A sufficient, stronger condition is that the whole Σ-algebra (C,(hop)opΣ) satisfies every generating equation under every carrier-valued assignment and every generator assignment AC.

Proof of Corollary 22.9 — Quotient descent

Proof. First suppose the fold equalizes every tree-valued and Kleisli instance of every generator. Induct on the generated congruence with the strengthened hypothesis that, for every f, foldη,h(t=f)=foldη,h(t=f). The return and operation clauses make the fold a homomorphism, so equality is preserved by every return or operation context. A sequencing context at a generator is one of the assumed Kleisli instances; above an operation node, lemma 28.9 pushes that context pointwise into the branches. Induction on the congruence derivation therefore shows that the fold equalizes the least congruence generated by those instances and is constant on quotient classes. Conversely, a fold which descends is constant on quotient classes; the two sides of each tree-valued generator instance represent one quotient class, so their images are equal. This proves the exact criterion.

Finally, if the whole carrier algebra satisfies every generator, interpret a tree-valued assignment by applying the fold to each assigned tree and each response branch. For a Kleisli instance, lemma 28.9 reduces both sides to the carrier equation with the generator assignment afoldη,h(f(a)). The carrier equation then says exactly that the two instantiated trees have equal fold images. Hence whole-algebra validity is sufficient. No converse is asserted: without a surjectivity or generation hypothesis, descent constrains only the carrier elements reached by the fold. ◻

State and exceptions in both orders

Let Σs be a signature disjoint from get and put. A tempting definition gives one map States for every initial store s. That family does calculate the examples, but it is not one instance of definition 22.7: its put clause changes the subscript. Put the store in the carrier instead. Define State:T{get,put}ΣsA(STΣs(A×S)) as the fold with carrier Ks:=STΣs(A×S) and algebra r(a)(s)=Ret(a,s),hget((),k)(s)=k(s)(s),hput(s,k)(s)=k(())(s),hop(p,k)(s)=Opop(p,λq.k(q)(s))(opΣs). Here k:RopKs in every operation clause. In particular, the read clause computes k(s)(s): the current store is both the response and the next state. The write clause computes k(())(s), discarding the old state. Expanding the fold gives the useful equations State(Ret(a))(s)=Ret(a,s),State(Opget((),k))(s)=State(k(s))(s),State(Opput(s,k))(s)=State(k(()))(s),State(Opop(p,k))(s)=Opop(p,λq.State(k(q))(s))(opΣs). Thus parameter passing, rather than a hidden global store, accounts for the changing state.

Let a0:A. For a signature Σe disjoint from raise, define Catcha0:T{raise}ΣeATΣeA by preserving returns, replacing every raise node by Ret(a0), and forwarding operations in Σe. These are folds; their omitted forwarding equations have exactly the last form above.

For the composed transaction below, fix a residual signature Θ disjoint from all three named operations and instantiate the two generic handlers differently: Σs={raise}Θ,Σe={get,put}Θ. Thus state handling may forward raise, while exception handling may forward get and put; the two residual signatures are intentionally not the same.

Now expand the transaction. Handling state first threads the updated store to the raise node and then forwards it: State(transaction)(s0)=State(Opput(s0+1,λ_.Opraise(e0,absurd0)))(s0)=State(Opraise(e0,absurd0))(s0+1)=Opraise(e0,absurd0). The first equality is the get clause, the second is the put clause evaluated at the new store s0+1, and the last is residual-operation forwarding. The successful result type would have been A×S, but no such leaf was produced. Catching outside it with fallback (a0,s0) gives Catch(a0,s0)(State(transaction)(s0))=Ret(a0,s0). The threaded update has disappeared: this order implements rollback.

In the other order, Catcha0 forwards the read and write while placing itself around their continuations, then replaces the raise by a successful return: Catcha0(transaction)=Opget((),λs.Catcha0(Opput(s+1,λ_.Opraise(e0,absurd0))))=Opget((),λs.Opput(s+1,λ_.Catcha0(Opraise(e0,absurd0))))=Opget((),λs.Opput(s+1,λ_.Ret(a0))). Consequently State(Catcha0(transaction))(s0)=Ret(a0,s0+1). The update remains. The two results differ because folds need not commute, not because either fold violates a monad law.

Exercise 22.5

★☆☆ For choose:1Bool, define a fold from T{choose}A to finite lists of A which explores the false branch before the true branch. Verify its operation equation.

A small call-by-push-value calculus

Operation trees explain sequencing but do not yet isolate why evaluation order changes a program. The effect-free calculus CBPV0 separates values from computations and contains exactly base values, thunks, returned values, sequencing, and functions from values to computations. The call-by-value and call-by-name translations use precisely those constructors. Read FA as “do a computation and return an A,” and UC as “be a suspended computation of type C.” The core is effect free: an FA-computation returns an A and requests nothing. Operations are added after the two translations are proved, so that the value/computation distinction can be seen on its own.

Definition 22.10 — CBPV_0

This local calculus writes 1 for the unit base type and 0 for an uninhabited base type. They play the roles earlier calculi gave to Unit and an empty type, but 0 here carries no subtyping rule such as chapter 8’s Bot. Let b range over base types. Among them, 0 is distinguished as empty: no constant or other closed value constructor has type 0. The unit type 1 has the constant (); later examples also declare base types S,Exc and a constant e0:Exc. Every other constant c has one declared base type. Thus the closed values of type 0 form the empty set denoted 0 in section 22.1; a continuation with domain 0 cannot be called in a closed program. The metavariables A,C,V,M range, respectively, over value types, computation types, values, and computations: A::=bUC,C::=FAAC,V::=xcthunkM,M::=returnVM to x.N::=forceVλx.MMV. Here UC classifies suspended computations, while FA classifies computations which return a value of type A. The arrow AC is a computation type: its argument is a value, and its body is a computation.

There are two judgments, ΓvV:A and ΓcM:C. Contexts contain value variables only. The complete rules are as follows.

x:AΓ
Γvx:A
V-Var
c:b is declared
Γvc:b
V-Const
ΓcM:C
ΓvthunkM:UC
V-Thunk
ΓvV:A
ΓcreturnV:FA
C-Return
ΓcM:FAΓ,x:AcN:C
ΓcM to x.N:C
C-To
ΓvV:UC
ΓcforceV:C
C-Force

Rules C-Return and C-To are the syntactic counterparts of the unit and bind of definition 22.2: return creates a successful leaf, and sequencing places the second computation after the first. No monad equations for CBPV0 are claimed here; U merely makes a computation into a value that can be delayed and duplicated. The later first-order reification theorem is the explicit connection back to operation-tree bind.

Γ,x:AcM:C
Γcλx.M:AC
C-Lam
ΓcM:ACΓvV:A
ΓcMV:C
C-App

Weak evaluation does not enter a thunk or a lambda. Its frames and redexes are K::=[]K to x.NKV,(returnV) to x.NN[V/x],Toforce(thunkM)M,Force(λx.M)VM[V/x],BetaK[M]K[M]if MM,Frame. A terminal computation is returnV or λx.M. This definition makes the asymmetry visible: a thunk is a value, whereas forcing it starts a computation.

Example 22.11 — A complete force-and-sequence derivation

Let c:b be declared. The inner return and its thunk have derivation vc:bVConst,creturnc:FbCReturn,vthunk(returnc):U(Fb)VThunk,cforce(thunk(returnc)):FbCForce,x:bcreturnx:FbCReturn,cforce(thunk(returnc)) to x.returnx:FbCTo. The rules just named also calculate it: force(thunk(returnc)) to x.returnx(returnc) to x.returnxreturnc. The first step is Force in a sequencing frame and the second is To. A thunk by itself would not take the first step.

Lemma 22.12 — Structural lemmas for CBPV_0

Value and computation typing admit weakening. If Γ,x:AvV:B and ΓvW:A, then ΓvV[W/x]:B. If Γ,x:AcM:C and ΓvW:A, then ΓcM[W/x]:C.

Proof of Lemma 22.12 — Structural lemmas for CBPV_0

Proof. Weakening is simultaneous induction over the two typing derivations. For substitution, induct simultaneously as well. The variable case either is x, when the conclusion is the assumed typing of W, or is a different variable, whose context lookup is unchanged. Constants contain no variables. The thunk case invokes the computation induction hypothesis. Return invokes the value hypothesis. Sequencing and lambda use α-renaming to keep their bound variable fresh, then apply the appropriate computation hypothesis to each premise. Force applies the value hypothesis to its thunk premise; application applies the computation hypothesis to its operator and the value hypothesis to its argument. Variables, constants, thunks, returns, sequencing, lambdas, force, and application exhaust the two syntactic categories. ◻

Lemma 22.13 — Canonical terminal computations

If cM:FA and M is terminal, then M=returnV for some V with vV:A. If cM:AC and M is terminal, then M=λx.N with x:AcN:C.

Proof of Lemma 22.13 — Canonical terminal computations

Proof. Inspect the two terminal forms. Inversion of C-Return excludes a computation arrow, and inversion of C-Lam excludes FA. The remaining inversions give the stated premises. ◻

Theorem 22.14 — Safety of CBPV_0

If ΓcM:C and MM, then ΓcM:C. If cM:C, then M is terminal or there is an M with MM.

Proof of Theorem 22.14 — Safety of CBPV_0

Proof. For preservation, induct on the reduction derivation. In the three root cases, inversion of typing followed by lemma 22.12 types respectively N[V/x], M, and M[V/x]. A frame case uses the induction hypothesis and reconstructs C-To or C-App.

For progress, induct on typing. Values need no evaluation theorem because they occur only in value positions. Return and lambda are terminal. Force has a closed value of type UC; inversion of value typing says it is a thunk, so Force applies. In sequencing, the first computation steps or is terminal. If terminal, lemma 22.13 proves that it has the form returnV, so To applies. The application case is identical, using the arrow half of the canonical-forms lemma. ◻

Exercise 22.6

★☆☆ Show that a closed, well-typed terminal computation cannot be both a return and a lambda. Which inversion fact, rather than an informal syntactic remark, proves the claim?

Call by value and call by name, factored

The source calculus is the simply typed lambda calculus over the same base types: τ::=bτσ,e::=xcλx.eee. Its typing judgment Γe:τ is generated by the complete rules

x:τΓ
Γx:τ
S-Var
c:b is declared
Γc:b
S-Const
Γ,x:τe:σ
Γλx.e:τσ
S-Lam
Γe1:τσΓe2:τ
Γe1e2:σ
S-App

For example, if c:b is declared, the application used below is typed by f:bbf:bbSVar,f:bbc:bSConst,f:bbfc:bSApp,λf.fc:(bb)bSLam,x:bx:bSVar,λx.x:bbSLam,(λf.fc)(λx.x):bSApp. No effect or evaluation-order premise is hidden in these rules. The two evaluations differ only at application. Writing w for a constant or lambda, call by value has

wvw
V-Val
e1vλx.ee2vw2e[w2/x]vw
e1e2vw
V-App

whereas call by name has

wnw
N-Val
e1nλx.ee[e2/x]nw
e1e2nw
N-App

Thus the call-by-name premise substitutes an unevaluated expression.

The call-by-value translation

Source types translate to CBPV value types: bv=b,(τσ)v=U(τvFσv). Values have a value translation wv, while all terms have a computation translation ev: xv=x,xv=returnx,cv=c,cv=returnc,(λx.e)v=thunk(λx.ev),(λx.e)v=return(thunk(λx.ev)). Application makes the sequencing order explicit: (e1e2)v=e1v to f.e2v to a.(forcef)a. The order of the two sequencings is the order of source evaluation.

The call-by-name translation

Call-by-name source types translate to CBPV computation types: bn=Fb,(τσ)n=U(τn)σn. A source variable x:τ is therefore represented by a thunk x:U(τn). Terms translate by xn=forcex,cn=returnc,(λx.e)n=λx.en,(e1e2)n=e1n(thunke2n). The argument is suspended before the function is entered. It is run only if an occurrence of x executes forcex.

Lemma 22.15 — Typing of both translations

If Γe:τ, then Γvcev:FτvandΓncen:τn, where x:τ becomes x:τv in Γv and x:U(τn) in Γn. If w is a source value of type τ, then Γvvwv:τv.

Proof of Lemma 22.15 — Typing of both translations

Proof. Induct on the source typing derivation. Variables and constants use C-Return in the value translation and respectively C-Force and C-Return in the name translation.

For an abstraction λx.e:τσ, the induction hypothesis gives Γv,x:τvcev:Fσv. Rules C-Lam, V-Thunk, and C-Return therefore give the call-by-value type F(U(τvFσv)). In the name translation the induction hypothesis is under x:U(τn), so C-Lam gives U(τn)σn directly.

For application, the value induction hypotheses are e1v:F(U(τvFσv)),e2v:Fτv. Two uses of C-To, followed by C-Force and C-App, give Fσv. The name hypotheses give e1n:U(τn)σn and thunke2n:U(τn), so one use of C-App finishes. Variables, constants, abstractions, and applications exhaust the source typing rules. The value claim is the variable, constant, and abstraction subcalculation just used. ◻

Call-by-value substitution is literal: (e[w/x])v=ev[wv/x]. Call-by-name substitution has one administrative force. Let a be the compatible closure, including positions below thunks and lambdas, of force(thunkM)aM. This subscripted relation is distinct from the static signature arrow : its operands are terms, and it contracts administrative redexes rather than declaring operation parameters and responses. Write Ma for the result of contracting all such redexes, and write MaN when Ma=Na up to alpha-equivalence. This is the least congruence containing the symmetric administrative equation. It is used to compare translations; a is not an additional weak evaluation rule.

Lemma 22.16 — Translation and substitution

For a source value w, (e[w/x])v=ev[wv/x]. For arbitrary u, (e[u/x])naen[thunkun/x].

Proof of Lemma 22.16 — Translation and substitution

Proof. Both claims are inductions on e, after renaming binders away from x and the free variables of the substituend. Only the matching-variable case is different. In the value translation both sides are returnwv. In the name translation the right side is force(thunkun) and the left side is un; these are the generating administrative equation. For application, the induction hypotheses give (e1[u/x])nae1n[thunkun/x],(e2[u/x])nae2n[thunkun/x]. Congruence places the first equality in function position and the second inside the thunked argument. Abstraction uses the body induction hypothesis beneath a freshly renamed binder. ◻

We write MW when CBPV weak reduction reaches a terminal computation W.

The name translation needs an operational fact stronger than congruence of a. An administrative equation may lie under a lambda, where weak evaluation cannot contract it, and become active after application. Lemma 22.17 transports weak evaluation across exactly that administrative equivalence.

Lemma 28.18 — Administrative normal forms and substitution

Administrative contraction is terminating and confluent. It commutes with value substitution in the following normalized sense: (M[V/x])a=(Ma[Va/x])a.

Proof of Lemma 28.18 — Administrative normal forms and substitution

Proof. Every administrative contraction deletes one occurrence of force and one occurrence of thunk, so no infinite contraction sequence exists. Two one-step contractions are either at disjoint positions, where they commute, or one lies inside the argument P of a redex force(thunkP). Contracting the outer redex first leaves P; contracting the inner one first leaves the corresponding contraction of P. The two results join in one step. Thus local confluence and termination give a unique normal form Ma.

Prove the displayed substitution equation by induction on M. Variable and constant cases are immediate, and constructors pass the induction hypotheses to their subterms. In the only exceptional case, substitution makes a value in force position into thunkP; the final normalization on the right contracts the newly formed force–thunk redex, just as normalization of M[V/x] does on the left. ◻

Lemma 28.19 — Weak steps across administrative normalization

The following two simulations hold: MM1there is Q with MaQ and Qa=(M1)a,MaLthere is M1 with M+M1 and (M1)a=La.

Proof of Lemma 28.19 — Weak steps across administrative normalization

Proof. A simultaneous induction on the weak-reduction derivation and its active frame proves the two displayed implications.

Weak step to normalized step. For the first implication, an administrative Force root is a stutter: take Q=Ma=(M1)a. The To and Beta roots use the substitution equation just proved. A sequencing or application frame reconstructs the corresponding weak step until the source root is exposed; take its weak reduct as Q. Administrative redexes newly exposed below a lambda may remain in Q, but normalization gives Qa=(M1)a; weak reduction is not claimed to enter that lambda.

Normalized step to weak trace. For the reverse implication, fix a normalization sequence MaMa and induct on its length, with an inner structural induction on the active weak-evaluation frame of the step from Ma. If the first administrative contraction is disjoint from that frame, commute it past the intended weak root and invoke the outer induction hypothesis on the shorter normalization suffix. If it is strictly nested in the frame’s substituend, the substitution equation replaces that nested contraction by the corresponding normalized substitution, after which the same suffix is shorter. If the active frame strictly contains the administrative redex, the inner frame induction removes its outer sequencing or application constructor and reconstructs it after the recursive call. The remaining case has the administrative redex at the active position: replay its Force step before the root exposed by normalization. For example, with M=(force(thunk(λx.N)))V, the normalized term takes one Beta step, while the original takes M(λx.N)VN[V/x]. The substitution equation gives (N[V/x])a=(Na[Va/x])a, so the endpoints required by the second implication agree. These Force, To, Beta, sequencing-frame, and application-frame cases exhaust active weak reductions. No weak step occurs below a thunk or lambda, so those two constructors contribute no additional case. ◻

Lemma 22.17 — Administrative transport for weak CBPV evaluation

If MaN and MW, then there is a terminal computation W such that NWandWaW. Because a is symmetric, exchanging M,W with N,W gives the converse: if NW, then MW for some terminal WaW. Moreover, administratively equivalent terminal computations have the same outer constructor: both are returns or both are lambdas.

Proof of Lemma 22.17 — Administrative transport for weak CBPV evaluation

Proof. Induction on the length of the weak trace, using lemma 28.19 and Qa=(M1)a to start the next normalized phase, shows that M terminates exactly when Ma terminates, with administratively equal terminal results. If MaN, lemma 28.18 gives both terms the same administrative normal form; transport the evaluation through that common term to obtain W. Finally, administrative contraction below a terminal return or lambda cannot change its outer constructor. ◻

For a source call-by-name value put cn=returnc,(λx.e)n=λx.en.

The reflection direction needs the phases of a target trace, not merely its final term. Weak CBPV reduction is deterministic because at most one root redex lies in the unique active frame. For a terminating M, let (M) be the length of this unique weak trace. For the name translation we use the administration-invariant cost a(M):=(Ma). Both and a are measures on CBPV0 only; the handler proofs below use neither. The transport lemma shows that this cost and the terminal administrative class depend only on the class of M.

Lemma 22.18 — Translated trace decomposition

Let the source terms below be closed and well typed.

  1. If (e1e2)vW, there are P and a value V such that e1vreturn(thunk(λx.P)),e2vreturnV,P[V/x]W. Each displayed subtrace is strictly shorter than the original trace.

  2. If Ma(e1e2)n and MW, there are P, L, and a terminal W such that e1nλx.P,LaP[thunke2n/x],LW,WaW. Moreover a(e1n)<a(M) and a(L)<a(M).

  3. Value translation is injective at terminal administrative normal forms: returned value translations are literally injective, and wnawn implies that w and w are alpha-equivalent.

Proof of Lemma 22.18 — Translated trace decomposition

Proof. For the value translation, the only active path through (e1e2)v first lies in e1v. Typing and canonical forms prove that its terminal value has the form thunk(λx.P). One To step exposes e2v; its terminal form is a returned value. The second To, then Force and Beta, expose P[V/x]. Determinism gives the displayed factorization and strictness of the three subtraces.

For the name translation, administrative normalization can change terms inside the operator or its thunked argument, but cannot remove the outer application. Its unique active path therefore first evaluates an operator administratively equivalent to e1n. Administrative transport replaces that phase by the displayed evaluation of e1n. Canonical forms prove that the terminal operator has the form λx.P. The following Beta step exposes a term administratively equivalent to P[thunke2n/x]; the substitution equation in lemma 22.17 gives LaP[thunke2n/x] and LW. Deleting the nonempty operator phase and beta step leaves the two proper subtraces, so a(e1n)<a(M) and a(L)<a(M).

Finally, first prove by induction on every source term e that e(en)a is injective up to alpha-equivalence. Variables, constants, abstractions, and applications are distinguished by their outer constructors; the recursive calls recover their immediate subterms. Now inspect terminal forms. Return, thunk, and lambda constructors are injective. Administrative contraction can occur inside a translated lambda body but cannot change its binder or outer constructor. The all-terms injectivity lemma recovers that body, and induction on the source value recovers the unique source value, up to the alpha-renaming already built into the syntax. ◻

Theorem 22.19 — Evaluation-order simulations

For every closed, well-typed source term:

  1. evw iff evreturnwv;

  2. enw iff enW for some terminal W with Wawn.

Thus the value translation preserves and reflects call-by-value evaluation, and the name translation preserves and reflects call-by-name evaluation up to the single declared administrative congruence.

Proof of Theorem 22.19 — Evaluation-order simulations

Proof. Call-by-value preservation. Induct on the displayed call-by-value source evaluation. A value is already translated to its stated terminal form. In the call-by-value application case, the first induction hypothesis reduces e1v to a returned thunk. The outer To step exposes the translation of e2; the second hypothesis reduces it to return(w2)v. The second To, Force, and Beta steps leave ev[(w2)v/x]=(e[w2/x])v. The third hypothesis reaches the required returned value.

Call-by-name preservation. For a call-by-name application, the operator hypothesis reaches λx.P with Paen. One Beta step leaves P[thunke2n/x]. Since Paen, congruence and lemma 22.16 give P[thunke2n/x]a(e[e2/x])n. The final source induction hypothesis evaluates the latter term. Administrative transport gives an evaluation of the former one with an equivalent terminal result.

Call-by-value reflection. Induct on the natural number (ev). A translated source value has only the terminal form stated above. For an application, the first clause of lemma 22.18 gives three strictly shorter subtraces. The first two induction hypotheses give e1vλx.e and e2vw2; injectivity of value translation identifies the terminal thunk and argument with (λx.e)v and (w2)v. The substitution equation turns the third subtrace into an evaluation of (e[w2/x])v, so the final induction hypothesis and V-App give e1e2vw.

Call-by-name reflection. Strengthen the claim as follows: Maen and MWthere is w with enw and Wawn. Induct on the administration-invariant natural number a(M). Constants are immediate. A translated lambda is terminal, and terminal-constructor invariance recovers that lambda. For an application, the second clause of lemma 22.18 gives a(e1n)<a(M) and a(L)<a(M). The operator induction hypothesis gives a source value; its target terminal is a lambda, so injectivity proves that the source value is λx.e and that Paen. Hence LaP[thunke2n/x]a(e[e2/x])n by translation and substitution. The second induction hypothesis gives e[e2/x]nw, and N-App gives e1e2nw. This proves the strengthened claim. Applying its terminal injectivity clause to the specified w proves the stated reflection direction. The source grammar has no further cases. ◻

Once operations are added in section 22.6, the opening obstruction can be stated inside the calculus. At the present effect-free stage, the simulation theorems fix the two evaluation orders and support the following informal reading. If tick() is represented by an operation request, its call-by-value translation occurs before the first sequencing can return the function result. Its call-by-name translation lies inside a thunk replacing x; when x is absent from the body, no force is generated and the request is never made.

Exercise 22.7

★☆☆ Translate (λf.fc)(λx.x) by call by value. Type every intermediate bound variable and reduce the CBPV term to its returned constant.

Exercise 22.8

★★☆ Prove the abstraction and application cases of the call-by-name substitution lemma. Explain why replacing a by weak reduction would make the abstraction case false.

Exercise 22.9

★☆☆ Let h:ττσ and u:τ be free variables. Translate (λx.hxx)u by call by name. Locate the two copies of the thunk for u and the two forces.

Operations in CBPV

The operation signature is a finite map Σ(op)=PopRop, but Pop and Rop are CBPV value types. Effects are finite sets E of operation names. A judgment ΓcM:C!E says that every operation exposed by evaluating M belongs to E. It is an upper bound: unused members are permitted. This choice makes weakening explicit and avoids pretending that the set is an inferred principal effect. This is deliberately not the row syntax of chapter 4: sets are idempotent and admit ordinary subset weakening, whereas the unique-label record rows of that chapter use a lacks constraint to prevent duplicates. The duplicate-sensitive effect-row alternative is developed next.

Latent effects must remain in types when a computation is suspended or a function is returned. The annotated types are A::=bUEC,C::=FAAEC. We recover the effect-free notation by writing UC and AC when the annotation is empty.

The term grammar gains an operation request opV(x.M) and a handler application handleMwithH. The request sends V:Pop and binds the eventual response x:Rop in M. For computations returning A, a handler has the form H={return xNr;opi(pi;ki)Ni}iI. Let handled(H)={opiiI}; operation names in a handler are distinct. The binder ki denotes the resumed, already handled continuation.

Definition 22.20 — Annotated typing rules

Value typing is as before except that thunk records its latent effect. The rules for the five old computation forms and for an operation request are:

ΓcM:C!E
ΓvthunkM:UEC
V-Thunk^Σ
ΓvV:A
ΓcreturnV:FA!
C-Return^Σ
ΓcM:FA!E1Γ,x:AcN:C!E2
ΓcM to x.N:C!(E1E2)
C-To^Σ
ΓvV:UEC
ΓcforceV:C!E
C-Force^Σ
Γ,x:AcM:C!E
Γcλx.M:AEC!
C-Lam^Σ
ΓcM:AE1C!E0ΓvV:A
ΓcMV:C!(E0E1)
C-App^Σ
Σ(op)=PRΓvV:PΓ,x:RcM:FA!E
ΓcopV(x.M):FA!({op}E)
C-Op
ΓcM:C!EEE
ΓcM:C!E
C-Weaken

The variable and constant rules are unchanged. A lambda has no immediate effect; the effect of entering its body is stored on its computation arrow. Likewise a thunk is a value whose type remembers the effect of forcing it.

The opening equation can now be settled inside the calculus. Add Σ(tick)=11 and abbreviate T=tick()(u.returnu). Extend both source translations by tick()v=T and tick()n=T. For the source term (λx:1.())tick(), call by value exposes the request: return(thunk(λx.return())) to f.T to a.(forcef)aToT to a.(force(thunk(λx.return())))a. This exposed term has type F1!{tick}. Call by name places the request in the unused argument thunk and erases it by beta: (λx.return())(thunkT)Betareturn(),creturn():F1!. Thus the two translations calculate the two observations with which the chapter began.

Definition 22.21 — Annotated handler judgment

Let H={return xNr;opi(pi;ki)Ni}iI,H=handled(H):={opiiI}. where the operation names are distinct. Write ΓhH:A[Ein]B[Eout] when the following premises hold:

  1. EinHEout;

  2. Γ,x:AcNr:FB!Eout;

  3. for every opiH with Σ(opi)=PiRi, Γ,pi:Pi,ki:U(RiEoutFB)cNi:FB!Eout.

The application rule for a handler is

ΓcM:FA!EinΓhH:A[Ein]B[Eout]
ΓchandleMwithH:FB!Eout
C-Handle

The set difference accounts for requests merely forwarded by the handler. Effects performed by a clause itself are in Eout because the clause body is outside the dynamic scope of this occurrence of the handler. The resumed continuation, by contrast, has latent effect Eout and will be put back under the handler.

The displayed judgment is declarative. A selected checker takes all value types, including latent effects in thunk and function types, as annotations. Value checking preserves those annotations; computation checking synthesizes only the least immediate effect. Write the concrete synthesis as outΞ(M), where Ξ records annotated variable types and hence the latent effects of forced thunks. This operation returns an effect or a type error.

Away from handlers, the checker is simultaneous structural recursion on values and computations. Return synthesizes . Force and application read their effects from the checked thunk or function type; sequencing takes the union of the two synthesized effects; and an operation takes {op} union the effect synthesized for its continuation body. Two rules cross an annotated computation boundary. To check thunkM at UDC, synthesize D0=outΞ(M) and require D0D. To check λx.M at ADC, impose the corresponding inclusion for the body under x:A. These inclusions check fixed latent annotations; they do not enlarge the enclosing computation’s immediate effect.

A handler cannot be checked by repeatedly calling this concrete operation at guesses for its output effect. The guessed set occurs inside each resumption’s value type, so a guess can make value checking fail even when a larger or different set is forced by a type annotation. Instead assign one effect unknown εh to every handler occurrence h in the checked term. For the operation clauses of h, run the same structural recursion symbolically under ki:U(RiεhFB). A checked term has finitely many such unknowns; call their set J. A symbolic immediate effect is a monotone map Ψ:P(Σ)JP(Σ). Constants, projections εh, union, and difference by a fixed handled set supply the maps produced by the recursion.

The symbolic pass returns three finite collections. Structural comparison of annotated value and computation types returns either a constructor clash, no condition, an equation εh=εh, or a pin εh=D. In particular, comparing U(RεhFB) with U(RDFB) produces that pin; repeated comparisons in the same equality class must produce the same D. Checking a symbolic body against a thunk or arrow annotation records an inclusion. If the expected latent annotation is an unknown εh, the inclusion is another lower-bound constraint on Eh and is added to Φh. If the expected annotation is a fixed set D, record the boundary condition (B)Θ(E¯)D with monotone Θ. Finally, for each handler occurrence h, let Ψin,h, Ψr,h, and Ψi,h be the symbolic immediate effects of its handled computation, return clause, and operation clauses. Define (H)Φh(E¯):=(Ψin,h(E¯)Hh)Ψr,h(E¯)iIhΨi,h(E¯). The constraint for h is Φh(E¯)Eh, where Φh also includes all symbolic boundary lower bounds whose right side is εh. Every Φh is monotone because projections, finite unions, and difference by a fixed set are monotone.

Normalize the effect equations by quotienting J into equality classes. Conflicting pins in one class reject. Hold every pinned class at its named set. On the product of the remaining, unpinned classes, start with the empty assignment and iterate simultaneously (I)EQn+1=EQnhQΦh(E¯n)(Q unpinned). For one unpinned outer handler whose handled computation has already been synthesized, the first stage contains EinH. Clause bodies and boundary lower bounds may contribute further effects at that same stage. At the product fixed point, verify every pinned handler constraint and every boundary condition. Return the component belonging to the checked computation’s outer handler, or its symbolic immediate effect evaluated at the solved assignment. The symbolic pass reports type-constructor clashes before solving.

Lemma 28.25 — Soundness and completeness of handler constraints

Fix the annotations in Ξ, all handler result types, and a finite operation signature. For every assignment E¯ to the handler unknowns, the declarative handler premises at the outputs Eh have derivations if and only if the symbolic pass has no constructor clash, every generated equality and pin holds under E¯, every generated boundary condition holds under E¯, and Φh(E¯)Eh(hJ). In that case each symbolic immediate effect evaluated at E¯ is the least immediate effect of its subterm with the resumptions annotated by the corresponding components of E¯, and the forward construction places weakening only at annotated boundaries.

Proof of Lemma 28.25 — Soundness and completeness of handler constraints

Proof. Proceed simultaneously by structural induction on values, computations, and handler clauses. A variable reads one annotated type from Ξ. At force and application, invert the required thunk, arrow, argument, and result types. Recursive structural comparison either matches their constructors or reports the same mismatch as declarative inversion. At an effect annotation, comparison with resumption types is literal equality after replacing every εh by Eh; this is exactly the generated equality or pin.

Return, sequencing, operation, force, and application reproduce the immediate effects in their typing rules, so union gives both soundness and minimality. An existing C-Weaken changes none of the generated expressions; it only composes the synthesized inclusion with the rule’s inclusion. At a thunk or lambda boundary, the induction hypothesis synthesizes Θ(E¯). If the expected annotation is fixed at D, the declarative premise exists exactly when Θ(E¯)D, which is condition (B). If it is εh, the same inversion yields the lower bound Θ(E¯)Eh placed in Φh. One C-Weaken constructs the premise when either inclusion is strict.

For a handler h, use the induction hypotheses for its handled computation and every clause. Its forwarded-input premise is precisely Ψin,h(E¯)HhEh after normalization. Indeed, if the original input annotation is Ein, the induction hypothesis gives Ψin,h(E¯)Ein; monotonicity of difference by Hh transports the original forwarding inclusion. Conversely, the synthesized input derivation supplies the smaller annotation Ψin,h(E¯) directly. The return and operation premises are precisely Ψr,h(E¯)Eh and Ψi,h(E¯)Eh. Their conjunction is Φh(E¯)Eh. The operation-premise environment substitutes the same Eh for every occurrence of εh, so the structural induction accounts for the resumption effect inside arbitrary value types, not merely forces of the resumption. Nested handlers are strict subterms and receive distinct unknowns; equations between their resumption types are retained in the same finite constraint system. These cases exhaust the grammar and prove both directions. ◻

Lemma 28.26 — Boundary normalization of effect weakening

Every declarative computation derivation can be transformed so that C-Weaken occurs at most once at each annotated computation boundary: at the root; immediately above the premise of V-Thunk or C-Lam, whose conclusion fixes a latent effect; and immediately above a handler clause premise, which must have the handler’s common output effect. No other occurrence of C-Weaken is needed.

Proof of Lemma 28.26 — Boundary normalization of effect weakening

Proof. Prove simultaneously, by structural induction on values, computations, and handler clauses, that the checker constructs a declarative derivation in the stated boundary form and that its synthesized immediate effect is contained in the conclusion effect of every declarative derivation with the same fixed value-type annotations. For return, force, application, sequencing, and operation, rebuild the rule from the smaller immediate effects. Each conclusion is the displayed union, which is contained in the corresponding union of the original premise effects. A C-Weaken in the original derivation merely composes this containment with its displayed inclusion.

For V-Thunk, the induction hypothesis gives an inner derivation at its synthesized effect D0. Since the original thunk type fixes D, inversion of its premise gives D0D. Insert one C-Weaken immediately above that premise when the inclusion is strict, then apply V-Thunk. The C-Lam case uses the corresponding construction with the fixed arrow effect: raise the body effect at the lambda boundary, while the lambda itself still has immediate effect . These two boundary steps cannot in general be moved to the root because their annotations occur in the result value type.

For a handler, generate the constraints of lemma 28.25. A given declarative output assignment E¯out satisfies them. A pinned equality class has the same component in every satisfying assignment. On the unpinned classes, simultaneous induction on (I) and monotonicity of every Φh give E¯E¯out componentwise. Each boundary expression Θ is monotone, so Θ(E¯out)D implies Θ(E¯)D. Thus the least assignment passes every boundary check. Constraint completeness supplies normalized clause derivations whose resumptions carry their handler components E,h. Put one C-Weaken at a clause boundary when its synthesized immediate effect is strictly smaller than E,h, and apply C-Handle. For the handler currently being normalized, put E,hEout,h at the root when its equality class is unpinned; a pinned class has equality there. This establishes the stated normal form and its shared handler-output invariant. ◻

Proposition 22.22 — Least synthesized effect

For fixed value-type annotations, the checker above terminates. It rejects exactly when no declarative typing exists and otherwise returns the least finite immediate effect admitted by the declarative rules. In an unpinned handler class, that effect is its component of the least simultaneous prefixed point; in a pinned class, it is the unique set named by the pins.

Proof of Proposition 22.22 — Least synthesized effect

Proof. Induct on the checked computation. By lemma 28.26, compare at each annotated boundary with a declarative derivation having the same fixed value types. Boundary weakening checks a latent annotation and does not contribute to the enclosing immediate effect. Every non-handler computation rule takes the union of the operation named at its root and the least immediate effects returned by the induction hypotheses, so any declarative annotation for that term contains the synthesized union.

In a handler case, symbolic generation terminates by structural recursion, because the set of handler occurrences and every generated constraint are finite. Equality-class normalization is finite. Monotonicity of all Φh proves that the unpinned product assignment grows componentwise in (I). If there are m unpinned classes, the finite product lattice permits at most m|Σ| strict single-operation additions. Every declarative assignment satisfies the same pins and is a prefixed point of the unpinned system, so induction on n puts the iterates below it. Hence E¯ is the least assignment compatible with the pins and handler lower bounds. If a boundary test Θ(E¯)D fails, monotonicity shows that it also fails at every larger satisfying assignment; rejection is complete. A pinned class has the same concrete set in every satisfying assignment, so checking its handler constraints after iteration is also complete: if Φh(E¯)Eh, monotonicity makes the same inclusion fail at every larger assignment while the pinned right side remains fixed. In every accepted case, lemma 28.25 constructs the three handler premises. ◻

The pin branch is necessary even for a pure clause. Put KE:=U(REF1) and consider an operation clause whose resumption has type k:KE in an environment containing g:U(KDF1). Let its body be (forceg)k, and let the return clause and the forwarded input be pure. Checking the argument of forceg compares KE with KD and generates the pin E=D. The body’s symbolic immediate effect is , so the remaining check is D. The checker returns D, exactly the declarative handler output. Iterating the old concrete clause checker from would instead encounter a type mismatch whenever D; it would reject before discovering the forced output annotation.

The distinction between latent and immediate effects is necessary. Fix the result annotation of return(thunk(return())) to be F(UD(F1)). The inner return synthesizes ; checking the thunk requires D and, when D, one boundary C-Weaken below V-Thunk. The outer return nevertheless synthesizes the least immediate effect . Moving the inner weakening to the root would change an immediate effect but would not establish the fixed latent annotation D.

Thus the repeated set in C-Handle is a checked invariant, not a circular input to the implementation.

For example, an exception handler from A to A with a pure fallback uses the repeated annotations Ein={raise}D,DEout,k:U(0EoutFA),Nraise:FA!Eout. The continuation variable is uncallable because its domain is 0. The ambient set D nevertheless appears in the input, the output, each clause judgment, and the continuation type. Nothing computes it: the programmer writes the same set four times.

The running transaction is now an actual annotated term. Declare S,Exc,1,0 as base types, with ():1, e0:Exc, and no closed constant of type 0. Also admit a fixed pure base-value update: if V:S, then V+:S. It denotes the same update written s+1 in the opening tree. Its complete typing rule is ΓvV:SΓvV+:SVNext. Put Mtx=get()(s.puts+(u.raisee0(z.returnz))). Its successful result type is 0. The complete bottom-up derivation is the following chain of rule instances: s:S,u:1,z:0vz:0,VVars:S,u:1,z:0creturnz:F0!,CReturnΣs:S,u:1craisee0(z.returnz):F0!{raise},COps:Svs+:S,VNexts:Scputs+(u.raisee0(z.returnz)):F0!Epr,COpcMtx:F0!Egpr.COp Here Epr={put,raise} and Egpr={get,put,raise}. The side premises in the last three rows are exactly the declared parameter types and the continuation judgment displayed in the preceding row. Thus no invocation is assigned an effect smaller than its operation name.

Now let Habort have return clause xreturn() and the single operation clause raise(p;k)return(). It discards the successful value and catches an exception, but forwards state operations. With Es={get,put}, the complete handler instance is cMtx:F0!EgprEgpr{raise}Esx:0creturn():F1!Esp:Exc,k:U(0EsF1)creturn():F1!EshHabort:0[Egpr]1[Es]HandlerchandleMtxwithHabort:F1!EsCHandle. The two clause-body premises use C-ReturnΣ followed by C-Weaken; the set-difference premise is equality.

Exercise 22.10

★☆☆ Derive the type of thunk(λx.raisee0(z.returnz)), giving x the value type 1 and e0 the exception type Exc. Distinguish the thunk’s immediate effect, the arrow’s immediate effect, and the arrow’s latent effect.

Exercise 22.11

★☆☆ Write all premises for a handler of choose which resumes first with false, discards that result, and then resumes with true. Which occurrence of the output effect set types each resumption?

Deep handling and forwarding

The old evaluation frames are extended by handleKwithH. For a fixed operation op, a handler-free op-open context is generated by Xop::=[]Xop to x.N. There is no application frame: an exposed request has type FA, not a function computation type. Because Xop contains no handler, an intervening handler without an op-clause must take Handle-Forward before an outer handler can match the request. This positive grammar, together with the ordinary evaluation-context grammar, selects the nearest handler without a negative “cannot step” premise. For example, if an inner H0 has no op-clause, then handle(opV(x.N))withH0 first forwards the request; only the resulting outer request can be caught by an enclosing handler that has such a clause. Induction on contexts gives unique decomposition into an ordinary redex, a return at its nearest handler, or a request in one such Xop. The convention is separate from the trace length , which was defined only for CBPV0.

Suppose H={return xNr;;op(p;k)Nop;}. The two handling reductions are Handle-Return, handle(returnV)withHNr[V/x], and Handle-Op, handleXop[opV(x.M)]withHNop[V/p,k^/k], where k^=thunk(λy.handleXop[M[y/x]]withH). The same H occurs inside k^. A call (forcek)W therefore resumes under the same handler. This is the defining difference between a deep handler and a shallow one. A shallow variant substitutes instead k^shallow=thunk(λy.Xop[M[y/x]]), so operations exposed after resumption escape this occurrence of H.

If ophandled(H), the handler forwards the request: handleXop[opV(x.M)]withHHandleForwardopV(y.handleXop[M[y/x]]withH). Forwarding rebuilds the operation with a handled continuation. It neither discards the request nor treats it as a returned value.

For the transaction and abort handler above, the first two requests are therefore visibly forwarded. If an enclosing state interpreter answers get with s0 and resumes put with (), the selected roots are handleMtxwithHabortHandleForwardget()(s.handleputs+(u.raisee0(z.returnz))withHabort),handleputs0+(u.raisee0(z.returnz))withHabortHandleForwardputs0+(u.handleraisee0(z.returnz)withHabort),handleraisee0(z.returnz)withHabortHandleOpreturn(). The write is forwarded before the exception is caught; this handler therefore does not roll state back.

The operation case calculated

Assume Σ(choose)=1Bool and let Htwice have return clause xreturnx and choose clause choose(p;k)(forcek)false to _.(forcek)true. The first result is deliberately discarded; the second is returned. For M=choose()(b.returnb), the handler step substitutes k^=thunk(λb.handle(returnb)withHtwice). The two resumptions can be read directly from the annotated calculation handleMwithHtwiceHandleOp(forcek^)false to _.(forcek^)trueForce(λb.handle(returnb)withHtwice)false to _.(forcek^)trueBetahandle(returnfalse)withHtwice to _.(forcek^)trueHandleReturnreturnfalse to _.(forcek^)trueTo(forcek^)trueForce,Beta,HandleReturnreturntrue. Both resumptions contain the handler again. Under a shallow rule, a later choice in the resumed continuation would escape instead of being collected.

This calculus gives resumptions multi-shot semantics: k is an ordinary thunked value, so Htwice may force it twice. A one-shot runtime instead consumes a continuation at its first resumption and reports an error on the second; that policy permits the continuation to be represented by a movable stack segment. A direct tree implementation of k^ instead retains or copies the captured Xop, with cost proportional to that context unless it is shared persistently. Thus Htwice intentionally marks the boundary between the free-model semantics proved here and a one-shot runtime; porting it requires explicit continuation cloning or a different handler.

The transaction Mtx belongs to the first-order reification fragment of section 22.9, as does the Htwice clause once its two calls are written with the abbreviation resumekV:=(forcek)V. A state handler that returns the final state, however, needs a product or a state-indexed function carrier, neither of which occurs in the deliberately small annotated grammar. Consequently this section does not pretend to give a direct operational rollback trace in an inexpressive carrier. The two state/exception-order calculations of subsection 22.3.1 are the complete account for this core; adding products yields the standard operational handler without changing the handling roots.

Safety with exposed operations

An operation request awaiting an enclosing interpreter is not a malformed program. Progress must therefore name it. Treating such a request as ordinary stuckness would make every modular effectful computation unsafe; treating it as a value would make the handler rule false.

Lemma 22.23 — Substitution and replacement

The annotated value, computation, and handler judgments admit weakening and value substitution. In addition, let a derivation of ΓcX[M]:C!E contain at its hole a subderivation ΔcM:D!E0. If ΔcM:D!E0, then replacing that occurrence gives ΓcX[M]:C!E.

Proof of Lemma 22.23 — Substitution and replacement

Proof. Weakening and substitution extend the simultaneous induction of lemma 22.12. In C-Op, substitute in the parameter value and in the continuation premise. In handler clauses, rename the parameter and continuation binders first; the continuation is an ordinary value variable of the displayed thunk type, so the value-substitution case applies.

For replacement, induct from the marked subderivation to the root. At every old frame, reconstruct C-ToΣ or C-AppΣ; their unions are unchanged because the replacement has the same effect annotation. At a handler frame, reconstruct C-Handle with the same handler judgment. If the path crosses C-Weaken, restore it with the same inclusion; this case may occur at any height between the marked hole and the root. No other evaluation frame exists. ◻

Lemma 22.24 — Typed operation decomposition

If ΓcXop[opV(x.M)]:C!E, then opE. Moreover, the derivation contains typings of ΓvV:Pop and, for some A and E0, of Γ,x:RopcM:FA!E0.

Proof of Lemma 22.24 — Typed operation decomposition

Proof. Induct outward through Xop. At the hole, C-Op contributes op to its effect; C-Weaken can only enlarge the set. The only inductive context clause is sequencing, whose effect union retains it. There is no application or handler case in the grammar of Xop. Inversion at C-Op gives ΓvV:Pop and Γ,x:RopcM:FA!E0 for its continuation result type and effect. ◻

Theorem 22.25 — Preservation

If ΓcM:C!E and MM, then ΓcM:C!E.

Proof of Theorem 22.25 — Preservation

Proof. The three old root reductions use substitution exactly as in theorem 22.14; frame reductions use replacement.

For a return-handler reduction, inversion gives Γ,x:AcNr:FB!Eout and ΓvV:A. Substitution types Nr[V/x].

For a handled operation, invert the typed decomposition of the exposed request inside Xop. For a fresh response variable y:Rop, response substitution first gives M[y/x]:FA!E0. Rule C-Weaken enlarges this annotation to {op}E0, exactly the annotation of the operation hole. Replacement now types the continuation Xop[M[y/x]] at the handled input type. Rule C-Handle then gives Γ,y:RopchandleXop[M[y/x]]withH:FB!Eout. Rules C-LamΣ and V-ThunkΣ type k^ as U(RopEoutFB). The operation-clause premise and two substitutions now type the reduct.

For forwarding, the same substitution–weakening–replacement calculation types the continuation of the rebuilt operation. The set-difference premise ensures that its operation name is in Eout; idempotence of union and, if needed, effect weakening derive the exact displayed output annotation. ◻

Theorem 22.26 — Progress up to an exposed operation

If cM:C!E, then one of the following kinds of outcome is available:

  1. M is terminal;

  2. MM for some M;

  3. M=Xop[opV(x.N)] for some opE, with no enclosing handler in Xop for that operation.

Proof of Theorem 22.26 — Progress up to an exposed operation

Proof. Induct on typing, stripping final uses of C-Weaken. Return and lambda are terminal. A well-typed force has a thunk value by canonical forms and takes Force; it cannot expose an operation. A well-typed application either evaluates its function or has a lambda and takes Beta; its active computation has function type, not (F A), so it cannot be the exposed request in item 3. Sequencing evaluates an (F A) premise; only in this case does an exposed request extend its open context by the current sequencing frame.

The two new term forms add no canonical value case: an operation request is nonterminal and produces the exposed-operation outcome, while a handler is nonterminal until its body returns, steps, or exposes a request. Thus the old canonical forms for thunks, lambdas, and returned values are unchanged rather than silently being assumed for new values.

An operation request is the third outcome with the empty context. For a handler, apply the induction hypothesis to its body. A return triggers the return rule. A step lifts through the handler frame. An exposed operation either has a clause, when the handled-operation rule applies, or lacks one, when forwarding applies. A lambda cannot type as the handler’s input FA. The operation membership assertion is lemma 22.24. ◻

Corollary 22.27 — Fully handled safety

If cM:C!, evaluation never reaches an unhandled operation. Every finite maximal reduction ends in a terminal computation of type C.

Proof of Corollary 22.27 — Fully handled safety

Proof. Preservation keeps the empty effect annotation. The third progress outcome would require op. Hence a finite maximal reduction can stop only at a terminal computation, which preservation keeps at type C. ◻

This is a partial-correctness theorem. The calculus in this chapter has no fixpoint, but a handler may resume more than once and can generate larger finite computations; adding general recursion would require a divergence case. No termination claim is smuggled into effect safety.

Exercise 22.12

★☆☆ Show that replacing the handler side condition EinHEout by EinEout remains safe but fails to express elimination. Give the inferred-looking output of an exception handler under that bad rule.

The syntax implements the tree fold

Definition 22.28 — First-order reification

The operational calculus contains higher-order values. The tree of definition 22.1 accounts for a smaller fragment. Here is its grammar. Its values are variables and closed base constants. Its computations are generated by L::=returnVopV(x.L)L to x.LresumekVhandleLwithH. A handler clause is a term L whose only free variables are its displayed base parameter and, in an operation clause, its continuation variable k. The notation resumekV elaborates to (forcek)V. There are no other lambdas, thunks, forces, or applications in this fragment.

A handler step replaces k by a thunked lambda. Its reduct therefore lies in the administrative closure of the grammar: besides the terms above, this closure admits exactly (force(thunk(λy.P)))Vand(λy.P)V at a resume site. Reification below is defined on this closure. The first of these terms reifies as the second, and the second reifies as P[V/y]. Thus the two ordinary CBPV steps at a resumed continuation do not change its tree. This is a partial definition: an arbitrary higher-order CBPV application has no reification here.

Given a base-value environment ρ and a continuation environment κ, set [[x]]ρ=ρ(x),[[c]]ρ=c for variables and closed base constants, and define [[returnV]]ρ,κ=Ret([[V]]ρ),[[opV(x.L)]]ρ,κ=Opop([[V]]ρ,λr.[[L]]ρ[xr],κ),[[L1 to x.L2]]ρ,κ=[[L1]]ρ,κ=(λa.[[L2]]ρ[xa],κ),[[resumekV]]ρ,κ=κ(k)([[V]]ρ). For a handler H, its algebra is rH(a)=[[Nr]][xa],,hH,op(p,g)=[[Nop]][popp],[kopg](ophandled(H)),hH,op(p,g)=Opop(p,g)(ophandled(H)). The clauses are closed apart from the indicated binders, so no omitted environment is needed. Finally define [[handleLwithH]]ρ,κ=foldrH,hH([[L]]ρ,κ). These equations are structural definitions on the displayed fragment; none appeals to an operational handler step.

For this section an op-open first-order context is Xopfo::=[]Xopfo to x.LhandleXopfowithG,ophandled(G). In particular, it has no general application frame. The only applications admitted above are administrative resumptions.

Lemma 22.29 — Reification through an open context

For every op-open first-order context Xopfo, after alpha-renaming the response variable x fresh for that context, [[Xopfo[opV(x.L)]]]ρ,κ=Opop([[V]]ρ,λr.[[Xopfo[L]]]ρ[xr],κ).

Proof of Lemma 22.29 — Reification through an open context

Proof. Write X for the displayed Xopfo inside this proof and induct on it. The empty context is the operation clause of reification. For X=X to z.N, apply the induction hypothesis and then the operation clause for bind: Op(p,g)=f=Op(p,λr.g(r)=f). The right side is the required reification of X[L] to z.N. For X=handleXwithG, the induction hypothesis rewrites the reification of X[opV(x.L)] to Opop(p,g) with the parameter and branch function from the lemma statement. Since G has no clause for op, its algebra sends (p,g) to Opop(p,g). The fold equation therefore rebuilds the node and places the fold around every response branch, which is the displayed equality. ◻

The operational rule substitutes a thunked continuation for k, whereas the algebra reads k from an environment. Lemma 22.30 proves equality of the two reifications.

Lemma 22.30 — Administrative reification of a deep continuation

Let k^=thunk(λy.handleX[L[y/x]]withH) where x is fresh for X, and put g(r)=[[handleX[L]withH]]ρ[xr],κ. For every first-order clause body N, [[N[k^/k]]]ρ,κ=[[N]]ρ,κ[kg], where the left side uses the two administrative reification clauses above. Base-value substitution likewise satisfies [[N[V/z]]]ρ,κ=[[N]]ρ[z[[V]]ρ],κ.

Proof of Lemma 22.30 — Administrative reification of a deep continuation

Proof. Both equations are inductions on N. Return, operation, handler, and sequencing cases follow by applying the induction hypotheses to their immediate subterms; sequencing also uses the defining bind equation. The only new continuation case becomes short if we put R(y):=handleX[L[y/x]]withH: (forcek^)Vreifies as(λy.R(y))Vreifies asR(V). Its reification is g([[V]]ρ) by base-value substitution, exactly the meaning assigned to resumekV by κ[kg]. ◻

For a closed term, write [[P]] for its denotation at the unique empty value and continuation environments.

Theorem 22.31 — Handler/tree agreement

Let P and P be closed, well-typed terms in the administrative closure of the first-order fragment. If PP is one of the following reductions, then [[P]]=[[P]].

  1. a return, handled-operation, or forwarding handler step;

  2. a sequencing step (returnV) to x.NN[V/x];

  3. either administrative continuation step displayed above;

  4. a compatible step in a first-order sequencing or handler frame.

Consequently, if M is a closed first-order computation and handleMwithHQ uses only these reductions and Q is a handler-free first-order computation, then [[Q]]=foldrH,hH([[M]]).

Proof of Theorem 22.31 — Handler/tree agreement

Proof. For a return-handler step, base-value substitution gives [[Nr[V/x]]]=rH([[V]]), which is the return equation of the fold.

For a handled operation, apply lemma 22.29. The left side of the operational rule reifies to hH,op(p,λr.[[handleX[L]withH]]ρ[xr],κ). The right side reifies to the same tree by parameter substitution and lemma 22.30. If the operation is unhandled, the algebra equation instead makes both sides Opop(p,λr.[[handleX[L]withH]]ρ[xr],κ), which proves the forwarding case.

For sequencing, reification turns the left side into Ret(a)=f=f(a), and base-value substitution identifies this with the right side. The two continuation steps preserve reification by its two administrative clauses. A sequencing frame applies tree bind to equal trees; a handler frame applies the same fold to equal trees. Sequencing, the two continuation contractions, sequencing frames, and handler frames exhaust the reductions allowed by the lemma, and each preserves the reified tree in one step.

Induction on the reduction sequence now gives [[handleMwithH]]=[[Q]]. Expanding the structural reification clause for the initial handler gives the claimed fold equation. ◻

The theorem is deliberately silent about general application frames, higher-order CBPV programs, and recursive computations. It also does not say that a syntactically definable handler respects an additional effect theory. That last obligation is the quotient-correctness condition following corollary 22.9.

Where algebraicity stops

An algebraic operation commutes with a surrounding sequencing context. In tree notation this was proposition 22.5; operationally it says that a context which runs after the response can be pushed into every response branch. Capturing the surrounding context is different.

One isolated calculation suffices. Suppose a delimiter and a capture form obey reset(E[shift k.M])reset(M[(λx.reset(E[x]))/k]). For this boundary calculation only, evaluation also has ordinary call-by-value beta reduction, arithmetic on numerals, and reset(n)n for a numeral n. Let v range over numerals and lambda abstractions. Delimiter-free capture contexts and full evaluation contexts are E::=[]EMvEE+Mn+EE×Mn×E,D::=[]DMvDD+Mn+DD×Mn×Dreset(D). Thus E cannot cross a reset, while D lets ordinary root reductions run beneath a delimiter. Besides the displayed shift and reset roots, use (λx.M)vM[v/x], the usual numeral arithmetic roots, and the frame rule MMD[M]D[M]. These clauses define the entire untyped control fragment considered here. In the absence of a typing judgment or answer-type index, they imply no type preservation or answer-type theorem. Take the arithmetic context E=1+[] and a body which ignores k. Then reset(1+shift k.0)0, whereas moving the context into the body gives reset(shift k.(1+0))1. The two sides of the would-be commutation law differ. The capture form observes the context which an algebraic operation must treat uniformly.

This is not yet a control calculus. No typing rule, answer type, or general translation for shift is being imported. Those choices belong to the later control development. The calculation establishes only the needed boundary: a fixed operation signature and its fold do not account for an operator whose behavior depends on capturing the ambient continuation.

Closed sets expose the row problem

The closed annotation system proves the safety theorem it claims, but it does not provide modular inference. Consider a higher-order function twice which applies an effectful callback twice. Its useful CBPV type has the schematic form U(AεFA)(AεFA). The callback’s unknown effect ε must reappear as the latent effect of the returned function. In the grammar of definition 22.20, however, an annotation is a concrete finite set. Writing one metavariable on the page does not define its formation, equality, generalization, or unification rules.

Local interpretation needs more. A state handler should express a family like ε.U{get,put}ε(FA)ε(SεFA), where the returned function accepts the initial state, the ambient effects pass through, and one handled layer of state is removed. Closed sets can instantiate this statement separately for each chosen ε, but cannot infer or generalize the family. Because sets are idempotent, they also cannot distinguish two scoped occurrences of the same operation name; removing the name removes every occurrence.

Polymorphic handlers require open effect collections, equations that expose one chosen label through an unknown tail, removal of one occurrence, and most-general unification. Unique-label record rows enforce a lacks constraint; effect rows instead permit duplicate labels, so removing one occurrence requires different equality and unification laws.

Exercise 22.13

★☆☆ Choose three concrete callback effect sets and write three separate types for twice in the closed-set grammar. Identify the repeated pieces which a quantified effect variable should abstract.

Suggested first pass.

Begin with exercise 22.14, exercise 22.17, exercise 22.18; then use the remaining problems to test deep resumption, forwarding, control, and duplicate-label boundaries.

Exercise 22.14

★★☆ Repeat both transaction calculations with a successful return after put. Then move the raise before the write. Display the two handler orders for each modified program and state which of these four outcomes differ.

Exercise 22.15

★★☆ Delete the handler around Xop[M[y/x]] in k^. Evaluate choose()(b1.choose()(b2.returnb2)) under Htwice. Identify the first request which becomes exposed and the exact missing handler occurrence that would capture it.

Exercise 22.16

★☆☆ Put a state request inside an exception handler which has no state clauses. Use Handle-Forward once, then place the result inside a state handler with a get clause. Display the rebuilt continuation and the nearest handler which captures the request.

Exercise 22.17

★★☆ Reconstruct the handled-operation case of preservation for choose, including the type of k^. Mark response substitution, effect weakening, replacement, parameter substitution, and continuation substitution separately.

Exercise 22.18

★★☆ Reify the complete Htwice calculation and identify the return and operation components of its algebra. Verify theorem 22.31 at its single choice node and at both resumption steps.

Exercise 22.19

★★☆ Use exactly the boundary rules displayed in section 22.10 and E=2×[]. Compare the original and context-pushed terms first with body k3 and then with body k(k3). Calculate all four normal forms. Identify the equality in the one-invocation case and the failure caused by invoking the captured doubling context twice.

Exercise 22.20

★★☆ Let H be a pure handler for op, and compare handle(op()(x.returnx))withH with the term obtained by placing a second copy of the same handler around that whole term. For each C-Handle instance, calculate Ein and Eout: the inner body has {op}, its result has , and the outer handler therefore receives . Then represent one and two available scoped layers by the proposed set annotations {op} and {op}{op}. Prove that they are equal and state the one-occurrence removal question that this annotation alone cannot answer.

Exercise 22.21

★★★ Practical project.handler-forwarding-machine Implement a finite evaluator for the annotated handler calculus. Maintain the invariant that forwarding retains a continuation recursively wrapped by the same deep handler, while a handled request invokes its clause with that reinstalled continuation. The eight cases must cover state/exception order, state forwarding, nested choice, and handler/fold agreement, ending with All 8 effects corpus cases passed.; the audit must be empty. Test three deliberately incorrect evaluators: make choice shallow, retain the old state after put, and discard a forwarded get. Each variant must remain executable and fail at least one of the eight expected outcomes. These finite tests are implementation evidence, not a proof of handler safety or contextual equivalence.

Sources.

Return and bind use Moggi’s Kleisli triple (Definition 1.2) and monad correspondence (Proposition 1.6) [Mog91]. Levy gives the CBPV decomposition and translations in Sections 3.7.1–3.7.2 [Lev01]. The effect-free Coq development gives the translations in Figures 5 and 8, translation support lemmas in Lemmas 2.2–2.12, and weak simulations in Section 3 [FSSS19]; it does not prove handler safety. Plotkin and Pretnar give syntax in Sections 2.1–2.4, rollback in Section 3.5, and free-model correctness in Section 4, while explicitly omitting formal operational semantics [PP13]. Thus the safety and agreement proofs are local. Bauer and Pretnar describe their prototype in Section 5 and supply executable examples in Section 6, not these metatheorems [BP15].

Search the book

Type to search the local edition.