The equation (𝜆𝑥.())𝑀=() is harmless when 𝑀 is a terminating pure term. It is false as an equation of programs as soon as evaluating 𝑀 can raise an exception or change a cell. Under call by value, 𝑀 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 𝑆 of stores and a set 𝖤𝗑𝖼 of exceptions, and write 1={∗} for a singleton set and 0=∅ for the empty set. Consider three operation symbols 𝗀𝖾𝗍:1⇝𝑆,𝗉𝗎𝗍:𝑆⇝1,𝗋𝖺𝗂𝗌𝖾:𝖤𝗑𝖼⇝0. 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 𝑒0:𝖤𝗑𝖼 and let 𝑠+1 denote a fixed update of 𝑠:𝑆. Define 𝗍𝗋𝖺𝗇𝗌𝖺𝖼𝗍𝗂𝗈𝗇=𝗀𝖾𝗍()𝗍𝗈𝑠.𝗉𝗎𝗍(𝑠+1)𝗍𝗈_.𝗋𝖺𝗂𝗌𝖾(𝑒0)𝗍𝗈𝑧.𝗋𝖾𝗍𝗎𝗋𝗇𝑧. The notation 𝑀𝗍𝗈𝑥.𝑁 is a bind: it runs the request or computation 𝑀, names its returned value 𝑥, and continues as 𝑁. The last continuation is unreachable because 𝑧: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 𝐴 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.
★☆☆ Let 𝗍𝗂𝖼𝗄 increment an observable counter and return unit. Compare the call-by-value and call-by-name evaluations of (𝜆𝑥.())𝗍𝗂𝖼𝗄(). Identify the exact step at which the two traces differ.
A finite operation signature Σ assigns to every operation symbol 𝗈𝗉 a parameter set 𝑃𝗈𝗉 and a response set 𝑅𝗈𝗉. We write Σ(𝗈𝗉)=𝑃𝗈𝗉⇝𝑅𝗈𝗉. No equations between operations are assumed yet.
For a set 𝐴, the set 𝑇Σ𝐴 is generated by 𝑡::=𝖱𝖾𝗍(𝑎)∣𝖮𝗉𝗈𝗉(𝑝,𝑘), where 𝑎:𝐴, 𝑝:𝑃𝗈𝗉, and 𝑘:𝑅𝗈𝗉→𝑇Σ𝐴. 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 𝑘.
For an operation request, write the smart constructor 𝗈𝗉(𝑝):=𝖮𝗉𝗈𝗉(𝑝,𝖱𝖾𝗍). Thus 𝗈𝗉(𝑝)𝗍𝗈𝑥.𝑁 expands to 𝖮𝗉𝗈𝗉(𝑝,𝜆𝑥.𝑁): 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 𝑇Σ𝐴↪𝑇Σ′𝐴 by 𝖱𝖾𝗍(𝑎)↦𝖱𝖾𝗍(𝑎) and 𝖮𝗉𝗈𝗉(𝑝,𝑘)↦𝖮𝗉𝗈𝗉(𝑝,𝜆𝑟.𝜄(𝑘(𝑟))).
The associated induction and recursion principles quantify over every response branch, even when there are infinitely many. To prove 𝑃(𝑡) for all 𝑡:𝑇Σ𝐴, it is enough to prove ∀𝑎:𝐴.𝑃(𝖱𝖾𝗍(𝑎)),∀𝗈𝗉,𝑝,𝑘.(∀𝑟:𝑅𝗈𝗉.𝑃(𝑘(𝑟)))⟹𝑃(𝖮𝗉𝗈𝗉(𝑝,𝑘)). The recursion principle has the same premises, with recursively computed values supplied for every 𝑘(𝑟). 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 𝑘 is the rest of the computation. For example, the transaction is the tree 𝖮𝗉𝗀𝖾𝗍((),𝜆𝑠.𝖮𝗉𝗉𝗎𝗍(𝑠+1,𝜆_.𝖮𝗉𝗋𝖺𝗂𝗌𝖾(𝑒0,𝖺𝖻𝗌𝗎𝗋𝖽0))), where 𝖺𝖻𝗌𝗎𝗋𝖽0:0→𝑇Σ𝐴 is the unique empty function. This expression is well typed for every 𝐴, but it has no 𝖱𝖾𝗍 leaf.
Sequencing must replace each successful return leaf of the first computation by the second computation, while retaining every pending operation.
For 𝑎:𝐴, 𝑡:𝑇Σ𝐴, and 𝑓:𝐴→𝑇Σ𝐵, define 𝗋𝖾𝗍𝗎𝗋𝗇𝑎:=𝖱𝖾𝗍(𝑎),𝖱𝖾𝗍(𝑎)≫=𝑓:=𝑓(𝑎),𝖮𝗉𝗈𝗉(𝑝,𝑘)≫=𝑓:=𝖮𝗉𝗈𝗉(𝑝,𝜆𝑟.𝑘(𝑟)≫=𝑓). The last line is recursion on the tree, not an equation assumed about an unspecified effect.
A monad in this chapter consists of a type constructor 𝑇, maps 𝗋𝖾𝗍𝗎𝗋𝗇𝐴:𝐴→𝑇𝐴,(≫=):(𝑇𝐴)→(𝐴→𝑇𝐵)→𝑇𝐵, 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.
For 𝑡:𝑇Σ𝐴, 𝑓:𝐴→𝑇Σ𝐵, and 𝑔:𝐵→𝑇Σ𝐶, 𝗋𝖾𝗍𝗎𝗋𝗇𝑎≫=𝑓=𝑓(𝑎),𝑡≫=𝗋𝖾𝗍𝗎𝗋𝗇=𝑡,(𝑡≫=𝑓)≫=𝑔=𝑡≫=(𝜆𝑎.𝑓(𝑎)≫=𝑔). 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 𝑡. At a return leaf, 𝖱𝖾𝗍(𝑎)≫=𝗋𝖾𝗍𝗎𝗋𝗇=𝖱𝖾𝗍(𝑎). At an operation node, the induction hypothesis gives 𝑘(𝑟)≫=𝗋𝖾𝗍𝗎𝗋𝗇=𝑘(𝑟) for every response 𝑟, hence 𝖮𝗉(𝑝,𝑘)≫=𝗋𝖾𝗍𝗎𝗋𝗇𝑏𝑖𝑛𝑑𝑎𝑡𝑎𝑛𝑜𝑝𝑒𝑟𝑎𝑡𝑖𝑜𝑛=𝖮𝗉(𝑝,𝜆𝑟.𝑘(𝑟)≫=𝗋𝖾𝗍𝗎𝗋𝗇)𝑖𝑛𝑑𝑢𝑐𝑡𝑖𝑜𝑛ℎ𝑦𝑝𝑜𝑡ℎ𝑒𝑠𝑖𝑠=𝖮𝗉(𝑝,𝜆𝑟.𝑘(𝑟))𝑚𝑒𝑡𝑎−𝑙𝑒𝑣𝑒𝑙𝑒𝑡𝑎=𝖮𝗉(𝑝,𝑘). The final equality is the meta-level eta equation 𝜆𝑟.𝑘(𝑟)=𝑘; pointwise equality of operation continuations then transports it through 𝖮𝗉.
For associativity, again induct on 𝑡. The return case is (𝖱𝖾𝗍(𝑎)≫=𝑓)≫=𝑔=𝑓(𝑎)≫=𝑔=𝖱𝖾𝗍(𝑎)≫=(𝜆𝑥.𝑓(𝑥)≫=𝑔). For an operation node, calculate every line: (𝖮𝗉(𝑝,𝑘)≫=𝑓)≫=𝑔𝑏𝑖𝑛𝑑𝑎𝑡𝑎𝑛𝑜𝑝𝑒𝑟𝑎𝑡𝑖𝑜𝑛=𝖮𝗉(𝑝,𝜆𝑟.𝑘(𝑟)≫=𝑓)≫=𝑔𝑏𝑖𝑛𝑑𝑎𝑡𝑎𝑛𝑜𝑝𝑒𝑟𝑎𝑡𝑖𝑜𝑛=𝖮𝗉(𝑝,𝜆𝑟.(𝑘(𝑟)≫=𝑓)≫=𝑔)𝑖𝑛𝑑𝑢𝑐𝑡𝑖𝑜𝑛ℎ𝑦𝑝𝑜𝑡ℎ𝑒𝑠𝑖𝑠=𝖮𝗉(𝑝,𝜆𝑟.𝑘(𝑟)≫=(𝜆𝑎.𝑓(𝑎)≫=𝑔))𝑏𝑖𝑛𝑑𝑎𝑡𝑎𝑛𝑜𝑝𝑒𝑟𝑎𝑡𝑖𝑜𝑛=𝖮𝗉(𝑝,𝑘)≫=(𝜆𝑎.𝑓(𝑎)≫=𝑔). The induction-hypothesis label is pointwise in every response 𝑟. ◻
The preceding calculation proves the monad laws for 𝑇Σ. For comparison, define 𝖬𝖺𝗒𝖻𝖾(𝐴)=𝐴+1 with 𝗋𝖾𝗍𝗎𝗋𝗇(𝑎)=𝗌𝗈𝗆𝖾(𝑎),𝗇𝗈𝗇𝖾≫=𝑓=𝗇𝗈𝗇𝖾,𝗌𝗈𝗆𝖾(𝑎)≫=𝑓=𝑓(𝑎). Put ℎ(𝑎)=𝑓(𝑎)≫=𝑔; then 𝗋𝖾𝗍𝗎𝗋𝗇(𝑎)≫=𝑓𝑑𝑒𝑓𝑖𝑛𝑖𝑡𝑖𝑜𝑛𝑜𝑓𝑟𝑒𝑡𝑢𝑟𝑛=𝗌𝗈𝗆𝖾(𝑎)≫=𝑓𝑏𝑖𝑛𝑑𝑎𝑡𝑠𝑜𝑚𝑒=𝑓(𝑎),𝗇𝗈𝗇𝖾≫=𝗋𝖾𝗍𝗎𝗋𝗇𝑏𝑖𝑛𝑑𝑎𝑡𝑛𝑜𝑛𝑒=𝗇𝗈𝗇𝖾,𝗌𝗈𝗆𝖾(𝑎)≫=𝗋𝖾𝗍𝗎𝗋𝗇𝑏𝑖𝑛𝑑𝑎𝑡𝑠𝑜𝑚𝑒=𝗋𝖾𝗍𝗎𝗋𝗇(𝑎)𝑑𝑒𝑓𝑖𝑛𝑖𝑡𝑖𝑜𝑛𝑜𝑓𝑟𝑒𝑡𝑢𝑟𝑛=𝗌𝗈𝗆𝖾(𝑎),(𝗇𝗈𝗇𝖾≫=𝑓)≫=𝑔𝑏𝑖𝑛𝑑𝑎𝑡𝑛𝑜𝑛𝑒=𝗇𝗈𝗇𝖾≫=𝑔𝑏𝑖𝑛𝑑𝑎𝑡𝑛𝑜𝑛𝑒=𝗇𝗈𝗇𝖾,𝗇𝗈𝗇𝖾≫=ℎ𝑏𝑖𝑛𝑑𝑎𝑡𝑛𝑜𝑛𝑒=𝗇𝗈𝗇𝖾,(𝗌𝗈𝗆𝖾(𝑎)≫=𝑓)≫=𝑔𝑏𝑖𝑛𝑑𝑎𝑡𝑠𝑜𝑚𝑒=𝑓(𝑎)≫=𝑔𝑑𝑒𝑓𝑖𝑛𝑖𝑡𝑖𝑜𝑛𝑜𝑓ℎ=ℎ(𝑎),𝗌𝗈𝗆𝖾(𝑎)≫=ℎ𝑏𝑖𝑛𝑑𝑎𝑡𝑠𝑜𝑚𝑒=ℎ(𝑎). 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.
For every operation node and every continuation 𝑓, 𝖮𝗉𝗈𝗉(𝑝,𝑘)≫=𝑓=𝖮𝗉𝗈𝗉(𝑝,𝜆𝑟.𝑘(𝑟)≫=𝑓). 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 𝑓; it is not a special property checked separately for reads, writes, or exceptions. ◻
The raw tree validates only equations forced by its constructors. We write a generating equation in a three-sorted context as Ξ⊢𝑠T=𝑡T:𝑇Σ𝐴. The context Ξ may contain ordinary value variables 𝑥:𝐵, computation variables 𝑚:𝑇Σ𝐵, and continuation variables 𝑘:𝐵→𝑇Σ𝐶. The sides 𝑠T,𝑡T are well-sorted tree expressions generated from those variables by 𝖱𝖾𝗍(𝑣), operation formation 𝖮𝗉𝗈𝗉(𝑝,𝜆𝑞.𝑠T), sequencing 𝑠T≫=𝜆𝑥.𝑡T, and continuation application 𝑘(𝑣). Here 𝑣,𝑝 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 𝑇Σ𝐴; 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 𝐴, then for every 𝑓:𝐴→𝑇Σ𝐵 its two sides may both be sequenced with 𝑓. Its congruence is the least equivalence containing all these instances and closed under every 𝖮𝗉 constructor and pointwise replacement of continuation branches. Equivalently, it is closed under sequencing: if 𝑡≈𝑡′, then 𝑡≫=𝑓≈𝑡′≫=𝑓, 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 𝖱𝖾𝗍(𝖿𝖺𝗅𝗌𝖾)≈𝖱𝖾𝗍(𝗍𝗋𝗎𝖾):𝑇Σ𝖡𝗈𝗈𝗅. Constructor congruence alone preserves that equation, but take 𝑓(𝖿𝖺𝗅𝗌𝖾)=𝖱𝖾𝗍(0) and 𝑓(𝗍𝗋𝗎𝖾)=𝖱𝖾𝗍(1). The two representatives bind to 𝖱𝖾𝗍(𝖿𝖺𝗅𝗌𝖾)≫=𝑓=𝖱𝖾𝗍(0)and𝖱𝖾𝗍(𝗍𝗋𝗎𝖾)≫=𝑓=𝖱𝖾𝗍(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: 𝗀𝖾𝗍()𝗍𝗈𝑠.𝗀𝖾𝗍()𝗍𝗈𝑠′.𝑘(𝑠,𝑠′)=𝗀𝖾𝗍()𝗍𝗈𝑠.𝑘(𝑠,𝑠),𝗀𝖾𝗍()𝗍𝗈𝑠.𝗉𝗎𝗍(𝑠)𝗍𝗈_.𝑚=𝑚,𝗉𝗎𝗍(𝑠)𝗍𝗈_.𝗀𝖾𝗍()𝗍𝗈𝑠′.𝑘(𝑠′)=𝗉𝗎𝗍(𝑠)𝗍𝗈_.𝑘(𝑠),𝗉𝗎𝗍(𝑠)𝗍𝗈_.𝗉𝗎𝗍(𝑠′)𝗍𝗈_.𝑚=𝗉𝗎𝗍(𝑠′)𝗍𝗈_.𝑚. 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.
★☆☆ 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.
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.
A Σ-algebra consists of a carrier 𝐶 and, for every operation, a map ℎ𝗈𝗉:𝑃𝗈𝗉×(𝑅𝗈𝗉→𝐶)→𝐶. Given a generator assignment 𝜂:𝐴→𝐶, its fold is the map defined by 𝖿𝗈𝗅𝖽𝜂,ℎ(𝖱𝖾𝗍(𝑎))=𝜂(𝑎),𝖿𝗈𝗅𝖽𝜂,ℎ(𝖮𝗉𝗈𝗉(𝑝,𝑘))=ℎ𝗈𝗉(𝑝,𝜆𝑞.𝖿𝗈𝗅𝖽𝜂,ℎ(𝑘(𝑞))).
Proof of Theorem 22.8 — Existence and uniqueness of handling
Proof. Existence is structural recursion on the well-founded tree. For uniqueness, prove 𝑞(𝑡)=𝖿𝗈𝗅𝖽𝜂,ℎ(𝑡) by induction on 𝑡. At a return leaf, both sides are 𝜂(𝑎). At an operation node, the defining equation for 𝑞 and the induction hypothesis at every response 𝑢 give 𝑞(𝖮𝗉(𝑝,𝑘))𝑑𝑒𝑓𝑖𝑛𝑖𝑛𝑔𝑒𝑞𝑢𝑎𝑡𝑖𝑜𝑛𝑓𝑜𝑟𝑞=ℎ𝗈𝗉(𝑝,𝜆𝑢.𝑞(𝑘(𝑢)))𝑝𝑜𝑖𝑛𝑡𝑤𝑖𝑠𝑒𝑖𝑛𝑑𝑢𝑐𝑡𝑖𝑜𝑛ℎ𝑦𝑝𝑜𝑡ℎ𝑒𝑠𝑖𝑠=ℎ𝗈𝗉(𝑝,𝜆𝑢.𝖿𝗈𝗅𝖽𝜂,ℎ(𝑘(𝑢)))𝑑𝑒𝑓𝑖𝑛𝑖𝑡𝑖𝑜𝑛𝑜𝑓𝑓𝑜𝑙𝑑=𝖿𝗈𝗅𝖽𝜂,ℎ(𝖮𝗉(𝑝,𝑘)). ◻
This is the universal property of the free Σ-algebra on 𝐴: 𝖱𝖾𝗍 inserts the generators, and a map out of 𝑇Σ𝐴 is uniquely determined by the generator assignment 𝜂 and the operation interpretations ℎ.
An algebra may ignore a continuation, call it once, or call it several times. Exception handling ignores the impossible continuation of 𝗋𝖺𝗂𝗌𝖾. 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 𝜂: 𝖿𝗈𝗅𝖽𝜂,ℎ(𝖮𝗉(𝑝,𝑘))=ℎ𝗈𝗉(𝑝,𝜆𝑞.𝖿𝗈𝗅𝖽𝜂,ℎ(𝑘(𝑞))).
For every 𝑡:𝑇Σ𝐴 and 𝑓:𝐴→𝑇Σ𝐵, 𝖿𝗈𝗅𝖽𝜂,ℎ(𝑡≫=𝑓)=𝖿𝗈𝗅𝖽𝖿𝗈𝗅𝖽𝜂,ℎ∘𝑓,ℎ(𝑡). The fold on the left has generator map 𝜂:𝐵→𝐶; the fold on the right has generator map 𝑎↦𝖿𝗈𝗅𝖽𝜂,ℎ(𝑓(𝑎)).
Proof. Induct using the tree induction principle following definition 22.1. At a return, both sides are 𝖿𝗈𝗅𝖽𝜂,ℎ(𝑓(𝑎)). At an operation node, 𝖿𝗈𝗅𝖽𝜂,ℎ(𝖮𝗉(𝑝,𝑘)≫=𝑓)=ℎ𝗈𝗉(𝑝,𝜆𝑟.𝖿𝗈𝗅𝖽𝜂,ℎ(𝑘(𝑟)≫=𝑓))=ℎ𝗈𝗉(𝑝,𝜆𝑟.𝖿𝗈𝗅𝖽𝖿𝗈𝗅𝖽𝜂,ℎ∘𝑓,ℎ(𝑘(𝑟)))=𝖿𝗈𝗅𝖽𝖿𝗈𝗅𝖽𝜂,ℎ∘𝑓,ℎ(𝖮𝗉(𝑝,𝑘)), where the middle equality is the pointwise induction hypothesis. ◻
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 (𝐶,(ℎ𝗈𝗉)𝗈𝗉∈Σ) satisfies every generating equation under every carrier-valued assignment and every generator assignment 𝐴→𝐶.
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 𝑓, 𝖿𝗈𝗅𝖽𝜂,ℎ(𝑡≫=𝑓)=𝖿𝗈𝗅𝖽𝜂,ℎ(𝑡′≫=𝑓). 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 𝑎↦𝖿𝗈𝗅𝖽𝜂,ℎ(𝑓(𝑎)). 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 Σ𝑠 be a signature disjoint from 𝗀𝖾𝗍 and 𝗉𝗎𝗍. A tempting definition gives one map 𝖲𝗍𝖺𝗍𝖾𝑠 for every initial store 𝑠. That family does calculate the examples, but it is not one instance of definition 22.7: its 𝗉𝗎𝗍 clause changes the subscript. Put the store in the carrier instead. Define 𝖲𝗍𝖺𝗍𝖾:𝑇{𝗀𝖾𝗍,𝗉𝗎𝗍}∪Σ𝑠𝐴→(𝑆→𝑇Σ𝑠(𝐴×𝑆)) as the fold with carrier 𝐾𝑠:=𝑆→𝑇Σ𝑠(𝐴×𝑆) and algebra 𝑟(𝑎)(𝑠)=𝖱𝖾𝗍(𝑎,𝑠),ℎ𝗀𝖾𝗍((),𝑘)(𝑠)=𝑘(𝑠)(𝑠),ℎ𝗉𝗎𝗍(𝑠′,𝑘)(𝑠)=𝑘(())(𝑠′),ℎ𝗈𝗉(𝑝,𝑘)(𝑠)=𝖮𝗉𝗈𝗉(𝑝,𝜆𝑞.𝑘(𝑞)(𝑠))(𝗈𝗉∈Σ𝑠). Here 𝑘:𝑅𝗈𝗉→𝐾𝑠 in every operation clause. In particular, the read clause computes 𝑘(𝑠)(𝑠): the current store is both the response and the next state. The write clause computes 𝑘(())(𝑠′), discarding the old state. Expanding the fold gives the useful equations 𝖲𝗍𝖺𝗍𝖾(𝖱𝖾𝗍(𝑎))(𝑠)=𝖱𝖾𝗍(𝑎,𝑠),𝖲𝗍𝖺𝗍𝖾(𝖮𝗉𝗀𝖾𝗍((),𝑘))(𝑠)=𝖲𝗍𝖺𝗍𝖾(𝑘(𝑠))(𝑠),𝖲𝗍𝖺𝗍𝖾(𝖮𝗉𝗉𝗎𝗍(𝑠′,𝑘))(𝑠)=𝖲𝗍𝖺𝗍𝖾(𝑘(()))(𝑠′),𝖲𝗍𝖺𝗍𝖾(𝖮𝗉𝗈𝗉(𝑝,𝑘))(𝑠)=𝖮𝗉𝗈𝗉(𝑝,𝜆𝑞.𝖲𝗍𝖺𝗍𝖾(𝑘(𝑞))(𝑠))(𝗈𝗉∈Σ𝑠). Thus parameter passing, rather than a hidden global store, accounts for the changing state.
Let 𝑎0:𝐴. For a signature Σ𝑒 disjoint from 𝗋𝖺𝗂𝗌𝖾, define 𝖢𝖺𝗍𝖼𝗁𝑎0:𝑇{𝗋𝖺𝗂𝗌𝖾}∪Σ𝑒𝐴→𝑇Σ𝑒𝐴 by preserving returns, replacing every raise node by 𝖱𝖾𝗍(𝑎0), and forwarding operations in Σ𝑒. 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: Σ𝑠={𝗋𝖺𝗂𝗌𝖾}∪Θ,Σ𝑒={𝗀𝖾𝗍,𝗉𝗎𝗍}∪Θ. Thus state handling may forward 𝗋𝖺𝗂𝗌𝖾, while exception handling may forward 𝗀𝖾𝗍 and 𝗉𝗎𝗍; 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: 𝖲𝗍𝖺𝗍𝖾(𝗍𝗋𝖺𝗇𝗌𝖺𝖼𝗍𝗂𝗈𝗇)(𝑠0)=𝖲𝗍𝖺𝗍𝖾(𝖮𝗉𝗉𝗎𝗍(𝑠0+1,𝜆_.𝖮𝗉𝗋𝖺𝗂𝗌𝖾(𝑒0,𝖺𝖻𝗌𝗎𝗋𝖽0)))(𝑠0)=𝖲𝗍𝖺𝗍𝖾(𝖮𝗉𝗋𝖺𝗂𝗌𝖾(𝑒0,𝖺𝖻𝗌𝗎𝗋𝖽0))(𝑠0+1)=𝖮𝗉𝗋𝖺𝗂𝗌𝖾(𝑒0,𝖺𝖻𝗌𝗎𝗋𝖽0). The first equality is the get clause, the second is the put clause evaluated at the new store 𝑠0+1, and the last is residual-operation forwarding. The successful result type would have been 𝐴×𝑆, but no such leaf was produced. Catching outside it with fallback (𝑎0,𝑠0) gives 𝖢𝖺𝗍𝖼𝗁(𝑎0,𝑠0)(𝖲𝗍𝖺𝗍𝖾(𝗍𝗋𝖺𝗇𝗌𝖺𝖼𝗍𝗂𝗈𝗇)(𝑠0))=𝖱𝖾𝗍(𝑎0,𝑠0). The threaded update has disappeared: this order implements rollback.
In the other order, 𝖢𝖺𝗍𝖼𝗁𝑎0 forwards the read and write while placing itself around their continuations, then replaces the raise by a successful return: 𝖢𝖺𝗍𝖼𝗁𝑎0(𝗍𝗋𝖺𝗇𝗌𝖺𝖼𝗍𝗂𝗈𝗇)=𝖮𝗉𝗀𝖾𝗍((),𝜆𝑠.𝖢𝖺𝗍𝖼𝗁𝑎0(𝖮𝗉𝗉𝗎𝗍(𝑠+1,𝜆_.𝖮𝗉𝗋𝖺𝗂𝗌𝖾(𝑒0,𝖺𝖻𝗌𝗎𝗋𝖽0))))=𝖮𝗉𝗀𝖾𝗍((),𝜆𝑠.𝖮𝗉𝗉𝗎𝗍(𝑠+1,𝜆_.𝖢𝖺𝗍𝖼𝗁𝑎0(𝖮𝗉𝗋𝖺𝗂𝗌𝖾(𝑒0,𝖺𝖻𝗌𝗎𝗋𝖽0))))=𝖮𝗉𝗀𝖾𝗍((),𝜆𝑠.𝖮𝗉𝗉𝗎𝗍(𝑠+1,𝜆_.𝖱𝖾𝗍(𝑎0))). Consequently 𝖲𝗍𝖺𝗍𝖾(𝖢𝖺𝗍𝖼𝗁𝑎0(𝗍𝗋𝖺𝗇𝗌𝖺𝖼𝗍𝗂𝗈𝗇))(𝑠0)=𝖱𝖾𝗍(𝑎0,𝑠0+1). The update remains. The two results differ because folds need not commute, not because either fold violates a monad law.
★☆☆ For 𝖼𝗁𝗈𝗈𝗌𝖾:1⇝𝖡𝗈𝗈𝗅, define a fold from 𝑇{𝖼𝗁𝗈𝗈𝗌𝖾}𝐴 to finite lists of 𝐴 which explores the false branch before the true branch. Verify its operation equation.
Operation trees explain sequencing but do not yet isolate why evaluation order changes a program. The effect-free calculus 𝖢𝖡𝖯𝖵0 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 𝐹𝐴 as “do a computation and return an 𝐴,” and 𝑈𝐶 as “be a suspended computation of type 𝐶.” The core is effect free: an 𝐹𝐴-computation returns an 𝐴 and requests nothing. Operations are added after the two translations are proved, so that the value/computation distinction can be seen on its own.
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 𝖴𝗇𝗂𝗍 and an empty type, but 0 here carries no subtyping rule such as chapter 8’s 𝖡𝗈𝗍. Let 𝑏 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 𝑆,𝖤𝗑𝖼 and a constant 𝑒0:𝖤𝗑𝖼. Every other constant 𝑐 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 𝐴,𝐶,𝑉,𝑀 range, respectively, over value types, computation types, values, and computations: 𝐴::=𝑏∣𝑈𝐶,𝐶::=𝐹𝐴∣𝐴⇒𝐶,𝑉::=𝑥∣𝑐∣𝗍𝗁𝗎𝗇𝗄𝑀,𝑀::=𝗋𝖾𝗍𝗎𝗋𝗇𝑉∣𝑀𝗍𝗈𝑥.𝑁::=∣𝖿𝗈𝗋𝖼𝖾𝑉∣𝜆𝑥.𝑀∣𝑀𝑉. Here 𝑈𝐶 classifies suspended computations, while 𝐹𝐴 classifies computations which return a value of type 𝐴. The arrow 𝐴⇒𝐶 is a computation type: its argument is a value, and its body is a computation.
There are two judgments, Γ⊢𝑣𝑉:𝐴 and Γ⊢𝑐𝑀:𝐶. Contexts contain value variables only. The complete rules are as follows.
𝑥:𝐴∈Γ
Γ⊢𝑣𝑥:𝐴
V-Var
𝑐:𝑏isdeclared
Γ⊢𝑣𝑐:𝑏
V-Const
Γ⊢𝑐𝑀:𝐶
Γ⊢𝑣𝗍𝗁𝗎𝗇𝗄𝑀:𝑈𝐶
V-Thunk
Γ⊢𝑣𝑉:𝐴
Γ⊢𝑐𝗋𝖾𝗍𝗎𝗋𝗇𝑉:𝐹𝐴
C-Return
Γ⊢𝑐𝑀:𝐹𝐴Γ,𝑥:𝐴⊢𝑐𝑁:𝐶
Γ⊢𝑐𝑀𝗍𝗈𝑥.𝑁:𝐶
C-To
Γ⊢𝑣𝑉:𝑈𝐶
Γ⊢𝑐𝖿𝗈𝗋𝖼𝖾𝑉:𝐶
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 𝖢𝖡𝖯𝖵0 are claimed here; 𝑈 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.
Γ,𝑥:𝐴⊢𝑐𝑀:𝐶
Γ⊢𝑐𝜆𝑥.𝑀:𝐴⇒𝐶
C-Lam
Γ⊢𝑐𝑀:𝐴⇒𝐶Γ⊢𝑣𝑉:𝐴
Γ⊢𝑐𝑀𝑉:𝐶
C-App
Weak evaluation does not enter a thunk or a lambda. Its frames and redexes are 𝐾::=[]∣𝐾𝗍𝗈𝑥.𝑁∣𝐾𝑉,(𝗋𝖾𝗍𝗎𝗋𝗇𝑉)𝗍𝗈𝑥.𝑁⟶𝑁[𝑉/𝑥],𝑇𝑜𝖿𝗈𝗋𝖼𝖾(𝗍𝗁𝗎𝗇𝗄𝑀)⟶𝑀,𝐹𝑜𝑟𝑐𝑒(𝜆𝑥.𝑀)𝑉⟶𝑀[𝑉/𝑥],𝐵𝑒𝑡𝑎𝐾[𝑀]⟶𝐾[𝑀′]if𝑀⟶𝑀′,𝐹𝑟𝑎𝑚𝑒. A terminal computation is 𝗋𝖾𝗍𝗎𝗋𝗇𝑉 or 𝜆𝑥.𝑀. This definition makes the asymmetry visible: a thunk is a value, whereas forcing it starts a computation.
Let 𝑐:𝑏 be declared. The inner return and its thunk have derivation ⋅⊢𝑣𝑐:𝑏𝑉−𝐶𝑜𝑛𝑠𝑡,⋅⊢𝑐𝗋𝖾𝗍𝗎𝗋𝗇𝑐:𝐹𝑏𝐶−𝑅𝑒𝑡𝑢𝑟𝑛,⋅⊢𝑣𝗍𝗁𝗎𝗇𝗄(𝗋𝖾𝗍𝗎𝗋𝗇𝑐):𝑈(𝐹𝑏)𝑉−𝑇ℎ𝑢𝑛𝑘,⋅⊢𝑐𝖿𝗈𝗋𝖼𝖾(𝗍𝗁𝗎𝗇𝗄(𝗋𝖾𝗍𝗎𝗋𝗇𝑐)):𝐹𝑏𝐶−𝐹𝑜𝑟𝑐𝑒,𝑥:𝑏⊢𝑐𝗋𝖾𝗍𝗎𝗋𝗇𝑥:𝐹𝑏𝐶−𝑅𝑒𝑡𝑢𝑟𝑛,⋅⊢𝑐𝖿𝗈𝗋𝖼𝖾(𝗍𝗁𝗎𝗇𝗄(𝗋𝖾𝗍𝗎𝗋𝗇𝑐))𝗍𝗈𝑥.𝗋𝖾𝗍𝗎𝗋𝗇𝑥:𝐹𝑏𝐶−𝑇𝑜. The rules just named also calculate it: 𝖿𝗈𝗋𝖼𝖾(𝗍𝗁𝗎𝗇𝗄(𝗋𝖾𝗍𝗎𝗋𝗇𝑐))𝗍𝗈𝑥.𝗋𝖾𝗍𝗎𝗋𝗇𝑥⟶(𝗋𝖾𝗍𝗎𝗋𝗇𝑐)𝗍𝗈𝑥.𝗋𝖾𝗍𝗎𝗋𝗇𝑥⟶𝗋𝖾𝗍𝗎𝗋𝗇𝑐. The first step is Force in a sequencing frame and the second is To. A thunk by itself would not take the first step.
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 𝑥, when the conclusion is the assumed typing of 𝑊, 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. ◻
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 𝐹𝐴. The remaining inversions give the stated premises. ◻
Proof. For preservation, induct on the reduction derivation. In the three root cases, inversion of typing followed by lemma 22.12 types respectively 𝑁[𝑉/𝑥], 𝑀, and 𝑀[𝑉/𝑥]. 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 𝑈𝐶; 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 𝗋𝖾𝗍𝗎𝗋𝗇𝑉, so To applies. The application case is identical, using the arrow half of the canonical-forms lemma. ◻
★☆☆ 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?
The source calculus is the simply typed lambda calculus over the same base types: 𝜏::=𝑏∣𝜏→𝜎,𝑒::=𝑥∣𝑐∣𝜆𝑥.𝑒∣𝑒𝑒. Its typing judgment Γ⊢𝑒:𝜏 is generated by the complete rules
𝑥:𝜏∈Γ
Γ⊢𝑥:𝜏
S-Var
𝑐:𝑏isdeclared
Γ⊢𝑐:𝑏
S-Const
Γ,𝑥:𝜏⊢𝑒:𝜎
Γ⊢𝜆𝑥.𝑒:𝜏→𝜎
S-Lam
Γ⊢𝑒1:𝜏→𝜎Γ⊢𝑒2:𝜏
Γ⊢𝑒1𝑒2:𝜎
S-App
For example, if 𝑐:𝑏 is declared, the application used below is typed by 𝑓:𝑏→𝑏⊢𝑓:𝑏→𝑏𝑆−𝑉𝑎𝑟,𝑓:𝑏→𝑏⊢𝑐:𝑏𝑆−𝐶𝑜𝑛𝑠𝑡,𝑓:𝑏→𝑏⊢𝑓𝑐:𝑏𝑆−𝐴𝑝𝑝,⋅⊢𝜆𝑓.𝑓𝑐:(𝑏→𝑏)→𝑏𝑆−𝐿𝑎𝑚,𝑥:𝑏⊢𝑥:𝑏𝑆−𝑉𝑎𝑟,⋅⊢𝜆𝑥.𝑥:𝑏→𝑏𝑆−𝐿𝑎𝑚,⋅⊢(𝜆𝑓.𝑓𝑐)(𝜆𝑥.𝑥):𝑏𝑆−𝐴𝑝𝑝. No effect or evaluation-order premise is hidden in these rules. The two evaluations differ only at application. Writing 𝑤 for a constant or lambda, call by value has
𝑤⇓𝑣𝑤
V-Val
𝑒1⇓𝑣𝜆𝑥.𝑒𝑒2⇓𝑣𝑤2𝑒[𝑤2/𝑥]⇓𝑣𝑤
𝑒1𝑒2⇓𝑣𝑤
V-App
whereas call by name has
𝑤⇓𝑛𝑤
N-Val
𝑒1⇓𝑛𝜆𝑥.𝑒𝑒[𝑒2/𝑥]⇓𝑛𝑤
𝑒1𝑒2⇓𝑛𝑤
N-App
Thus the call-by-name premise substitutes an unevaluated expression.
The call-by-value translation
Source types translate to CBPV value types: 𝑏𝑣=𝑏,(𝜏→𝜎)𝑣=𝑈(𝜏𝑣⇒𝐹𝜎𝑣). Values have a value translation 𝑤†𝑣, while all terms have a computation translation 𝑒𝑣: 𝑥†𝑣=𝑥,𝑥𝑣=𝗋𝖾𝗍𝗎𝗋𝗇𝑥,𝑐†𝑣=𝑐,𝑐𝑣=𝗋𝖾𝗍𝗎𝗋𝗇𝑐,(𝜆𝑥.𝑒)†𝑣=𝗍𝗁𝗎𝗇𝗄(𝜆𝑥.𝑒𝑣),(𝜆𝑥.𝑒)𝑣=𝗋𝖾𝗍𝗎𝗋𝗇(𝗍𝗁𝗎𝗇𝗄(𝜆𝑥.𝑒𝑣)). Application makes the sequencing order explicit: (𝑒1𝑒2)𝑣=𝑒𝑣1𝗍𝗈𝑓.𝑒𝑣2𝗍𝗈𝑎.(𝖿𝗈𝗋𝖼𝖾𝑓)𝑎. 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: 𝑏𝑛=𝐹𝑏,(𝜏→𝜎)𝑛=𝑈(𝜏𝑛)⇒𝜎𝑛. A source variable 𝑥:𝜏 is therefore represented by a thunk 𝑥:𝑈(𝜏𝑛). Terms translate by 𝑥𝑛=𝖿𝗈𝗋𝖼𝖾𝑥,𝑐𝑛=𝗋𝖾𝗍𝗎𝗋𝗇𝑐,(𝜆𝑥.𝑒)𝑛=𝜆𝑥.𝑒𝑛,(𝑒1𝑒2)𝑛=𝑒𝑛1(𝗍𝗁𝗎𝗇𝗄𝑒𝑛2). The argument is suspended before the function is entered. It is run only if an occurrence of 𝑥 executes 𝖿𝗈𝗋𝖼𝖾𝑥.
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 𝜆𝑥.𝑒:𝜏→𝜎, the induction hypothesis gives Γ𝑣,𝑥:𝜏𝑣⊢𝑐𝑒𝑣:𝐹𝜎𝑣. Rules C-Lam, V-Thunk, and C-Return therefore give the call-by-value type 𝐹(𝑈(𝜏𝑣⇒𝐹𝜎𝑣)). In the name translation the induction hypothesis is under 𝑥:𝑈(𝜏𝑛), so C-Lam gives 𝑈(𝜏𝑛)⇒𝜎𝑛 directly.
For application, the value induction hypotheses are 𝑒𝑣1:𝐹(𝑈(𝜏𝑣⇒𝐹𝜎𝑣)),𝑒𝑣2:𝐹𝜏𝑣. Two uses of C-To, followed by C-Force and C-App, give 𝐹𝜎𝑣. The name hypotheses give 𝑒𝑛1:𝑈(𝜏𝑛)⇒𝜎𝑛 and 𝗍𝗁𝗎𝗇𝗄𝑒𝑛2:𝑈(𝜏𝑛), 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: (𝑒[𝑤/𝑥])𝑣=𝑒𝑣[𝑤†𝑣/𝑥]. Call-by-name substitution has one administrative force. Let ⇝𝑎 be the compatible closure, including positions below thunks and lambdas, of 𝖿𝗈𝗋𝖼𝖾(𝗍𝗁𝗎𝗇𝗄𝑀)⇝𝑎𝑀. 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 𝑀𝑎 for the result of contracting all such redexes, and write 𝑀≡𝑎𝑁 when 𝑀𝑎=𝑁𝑎 up to alpha-equivalence. This is the least congruence containing the symmetric administrative equation. It is used to compare translations; ⇝𝑎 is not an additional weak evaluation rule.
Proof of Lemma 22.16 — Translation and substitution
Proof. Both claims are inductions on 𝑒, after renaming binders away from 𝑥 and the free variables of the substituend. Only the matching-variable case is different. In the value translation both sides are 𝗋𝖾𝗍𝗎𝗋𝗇𝑤†𝑣. In the name translation the right side is 𝖿𝗈𝗋𝖼𝖾(𝗍𝗁𝗎𝗇𝗄𝑢𝑛) and the left side is 𝑢𝑛; these are the generating administrative equation. For application, the induction hypotheses give (𝑒1[𝑢/𝑥])𝑛≡𝑎𝑒𝑛1[𝗍𝗁𝗎𝗇𝗄𝑢𝑛/𝑥],(𝑒2[𝑢/𝑥])𝑛≡𝑎𝑒𝑛2[𝗍𝗁𝗎𝗇𝗄𝑢𝑛/𝑥]. 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 𝑀⇓𝑊 when CBPV weak reduction reaches a terminal computation 𝑊.
The name translation needs an operational fact stronger than congruence of ≡𝑎. 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.
Proof of Lemma 28.18 — Administrative normal forms and substitution
Proof. Every administrative contraction deletes one occurrence of 𝖿𝗈𝗋𝖼𝖾 and one occurrence of 𝗍𝗁𝗎𝗇𝗄, so no infinite contraction sequence exists. Two one-step contractions are either at disjoint positions, where they commute, or one lies inside the argument 𝑃 of a redex 𝖿𝗈𝗋𝖼𝖾(𝗍𝗁𝗎𝗇𝗄𝑃). Contracting the outer redex first leaves 𝑃; contracting the inner one first leaves the corresponding contraction of 𝑃. The two results join in one step. Thus local confluence and termination give a unique normal form 𝑀𝑎.
Prove the displayed substitution equation by induction on 𝑀. 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 𝗍𝗁𝗎𝗇𝗄𝑃; the final normalization on the right contracts the newly formed force–thunk redex, just as normalization of 𝑀[𝑉/𝑥] does on the left. ◻
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 𝑄=𝑀𝑎=(𝑀1)𝑎. 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 𝑄. Administrative redexes newly exposed below a lambda may remain in 𝑄, but normalization gives 𝑄𝑎=(𝑀1)𝑎; weak reduction is not claimed to enter that lambda.
Normalized step to weak trace. For the reverse implication, fix a normalization sequence 𝑀⇝𝑎∗𝑀𝑎 and induct on its length, with an inner structural induction on the active weak-evaluation frame of the step from 𝑀𝑎. 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 𝑀=(𝖿𝗈𝗋𝖼𝖾(𝗍𝗁𝗎𝗇𝗄(𝜆𝑥.𝑁)))𝑉, the normalized term takes one Beta step, while the original takes 𝑀⟶(𝜆𝑥.𝑁)𝑉⟶𝑁[𝑉/𝑥]. The substitution equation gives (𝑁[𝑉/𝑥])𝑎=(𝑁𝑎[𝑉𝑎/𝑥])𝑎, 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. ◻
If 𝑀≡𝑎𝑁 and 𝑀⇓𝑊, then there is a terminal computation 𝑊′ such that 𝑁⇓𝑊′and𝑊≡𝑎𝑊′. Because ≡𝑎 is symmetric, exchanging 𝑀,𝑊 with 𝑁,𝑊′ gives the converse: if 𝑁⇓𝑊′, then 𝑀⇓𝑊 for some terminal 𝑊≡𝑎𝑊′. 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 𝑄𝑎=(𝑀1)𝑎 to start the next normalized phase, shows that 𝑀 terminates exactly when 𝑀𝑎 terminates, with administratively equal terminal results. If 𝑀≡𝑎𝑁, lemma 28.18 gives both terms the same administrative normal form; transport the evaluation through that common term to obtain 𝑊′. Finally, administrative contraction below a terminal return or lambda cannot change its outer constructor. ◻
For a source call-by-name value put 𝑐†𝑛=𝗋𝖾𝗍𝗎𝗋𝗇𝑐,(𝜆𝑥.𝑒)†𝑛=𝜆𝑥.𝑒𝑛.
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 𝑀, let ℓ(𝑀) be the length of this unique weak trace. For the name translation we use the administration-invariant cost ℓ𝑎(𝑀):=ℓ(𝑀𝑎). Both ℓ and ℓ𝑎 are measures on 𝖢𝖡𝖯𝖵0 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 𝑀.
Let the source terms below be closed and well typed.
If (𝑒1𝑒2)𝑣⇓𝑊, there are 𝑃 and a value 𝑉 such that 𝑒𝑣1⇓𝗋𝖾𝗍𝗎𝗋𝗇(𝗍𝗁𝗎𝗇𝗄(𝜆𝑥.𝑃)),𝑒𝑣2⇓𝗋𝖾𝗍𝗎𝗋𝗇𝑉,𝑃[𝑉/𝑥]⇓𝑊. Each displayed subtrace is strictly shorter than the original trace.
If 𝑀≡𝑎(𝑒1𝑒2)𝑛 and 𝑀⇓𝑊, there are 𝑃, 𝐿, and a terminal 𝑊′ such that 𝑒𝑛1⇓𝜆𝑥.𝑃,𝐿≡𝑎𝑃[𝗍𝗁𝗎𝗇𝗄𝑒𝑛2/𝑥],𝐿⇓𝑊′,𝑊′≡𝑎𝑊. Moreover ℓ𝑎(𝑒𝑛1)<ℓ𝑎(𝑀) and ℓ𝑎(𝐿)<ℓ𝑎(𝑀).
Value translation is injective at terminal administrative normal forms: returned value translations are literally injective, and 𝑤†𝑛≡𝑎𝑤′†𝑛 implies that 𝑤 and 𝑤′ are alpha-equivalent.
Proof of Lemma 22.18 — Translated trace decomposition
Proof. For the value translation, the only active path through (𝑒1𝑒2)𝑣 first lies in 𝑒𝑣1. Typing and canonical forms prove that its terminal value has the form 𝗍𝗁𝗎𝗇𝗄(𝜆𝑥.𝑃). One To step exposes 𝑒𝑣2; its terminal form is a returned value. The second To, then Force and Beta, expose 𝑃[𝑉/𝑥]. 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 𝑒𝑛1. Administrative transport replaces that phase by the displayed evaluation of 𝑒𝑛1. Canonical forms prove that the terminal operator has the form 𝜆𝑥.𝑃. The following Beta step exposes a term administratively equivalent to 𝑃[𝗍𝗁𝗎𝗇𝗄𝑒𝑛2/𝑥]; the substitution equation in lemma 22.17 gives 𝐿≡𝑎𝑃[𝗍𝗁𝗎𝗇𝗄𝑒𝑛2/𝑥] and 𝐿⇓𝑊′. Deleting the nonempty operator phase and beta step leaves the two proper subtraces, so ℓ𝑎(𝑒𝑛1)<ℓ𝑎(𝑀) and ℓ𝑎(𝐿)<ℓ𝑎(𝑀).
Finally, first prove by induction on every source term 𝑒 that 𝑒↦(𝑒𝑛)𝑎 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. ◻
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 𝑒𝑣1 to a returned thunk. The outer To step exposes the translation of 𝑒2; the second hypothesis reduces it to 𝗋𝖾𝗍𝗎𝗋𝗇(𝑤2)†𝑣. The second To, Force, and Beta steps leave 𝑒𝑣[(𝑤2)†𝑣/𝑥]=(𝑒[𝑤2/𝑥])𝑣. The third hypothesis reaches the required returned value.
Call-by-name preservation. For a call-by-name application, the operator hypothesis reaches 𝜆𝑥.𝑃 with 𝑃≡𝑎𝑒𝑛. One Beta step leaves 𝑃[𝗍𝗁𝗎𝗇𝗄𝑒𝑛2/𝑥]. Since 𝑃≡𝑎𝑒𝑛, congruence and lemma 22.16 give 𝑃[𝗍𝗁𝗎𝗇𝗄𝑒𝑛2/𝑥]≡𝑎(𝑒[𝑒2/𝑥])𝑛. 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 ℓ(𝑒𝑣). 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 𝑒1⇓𝑣𝜆𝑥.𝑒 and 𝑒2⇓𝑣𝑤2; injectivity of value translation identifies the terminal thunk and argument with (𝜆𝑥.𝑒)†𝑣 and (𝑤2)†𝑣. The substitution equation turns the third subtrace into an evaluation of (𝑒[𝑤2/𝑥])𝑣, so the final induction hypothesis and V-App give 𝑒1𝑒2⇓𝑣𝑤.
Call-by-name reflection. Strengthen the claim as follows: 𝑀≡𝑎𝑒𝑛and𝑀⇓𝑊⟹thereis𝑤with𝑒⇓𝑛𝑤and𝑊≡𝑎𝑤†𝑛. Induct on the administration-invariant natural number ℓ𝑎(𝑀). 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 ℓ𝑎(𝑒𝑛1)<ℓ𝑎(𝑀) and ℓ𝑎(𝐿)<ℓ𝑎(𝑀). The operator induction hypothesis gives a source value; its target terminal is a lambda, so injectivity proves that the source value is 𝜆𝑥.𝑒 and that 𝑃≡𝑎𝑒𝑛. Hence 𝐿≡𝑎𝑃[𝗍𝗁𝗎𝗇𝗄𝑒𝑛2/𝑥]≡𝑎(𝑒[𝑒2/𝑥])𝑛 by translation and substitution. The second induction hypothesis gives 𝑒[𝑒2/𝑥]⇓𝑛𝑤, and N-App gives 𝑒1𝑒2⇓𝑛𝑤. This proves the strengthened claim. Applying its terminal injectivity clause to the specified 𝑤 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 𝗍𝗂𝖼𝗄() 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 𝑥; when 𝑥 is absent from the body, no force is generated and the request is never made.
★★☆ Prove the abstraction and application cases of the call-by-name substitution lemma. Explain why replacing ≡𝑎 by weak reduction would make the abstraction case false.
The operation signature is a finite map Σ(𝗈𝗉)=𝑃𝗈𝗉⇝𝑅𝗈𝗉, but 𝑃𝗈𝗉 and 𝑅𝗈𝗉 are CBPV value types. Effects are finite sets E of operation names. A judgment Γ⊢𝑐𝑀:𝐶!E says that every operation exposed by evaluating 𝑀 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 𝐴::=𝑏∣𝑈E𝐶,𝐶::=𝐹𝐴∣𝐴⇒E𝐶. We recover the effect-free notation by writing 𝑈𝐶 and 𝐴⇒𝐶 when the annotation is empty.
The term grammar gains an operation request 𝗈𝗉𝑉(𝑥.𝑀) and a handler application 𝗁𝖺𝗇𝖽𝗅𝖾𝑀𝗐𝗂𝗍𝗁𝐻. The request sends 𝑉:𝑃𝗈𝗉 and binds the eventual response 𝑥:𝑅𝗈𝗉 in 𝑀. For computations returning 𝐴, a handler has the form 𝐻={𝗋𝖾𝗍𝗎𝗋𝗇𝑥↦𝑁𝑟;𝗈𝗉𝑖(𝑝𝑖;𝑘𝑖)↦𝑁𝑖}𝑖∈𝐼. Let 𝗁𝖺𝗇𝖽𝗅𝖾𝖽(𝐻)={𝗈𝗉𝑖∣𝑖∈𝐼}; operation names in a handler are distinct. The binder 𝑘𝑖 denotes the resumed, already handled continuation.
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:
Γ⊢𝑐𝑀:𝐶!E
Γ⊢𝑣𝗍𝗁𝗎𝗇𝗄𝑀:𝑈E𝐶
V-Thunk^Σ
Γ⊢𝑣𝑉:𝐴
Γ⊢𝑐𝗋𝖾𝗍𝗎𝗋𝗇𝑉:𝐹𝐴!∅
C-Return^Σ
Γ⊢𝑐𝑀:𝐹𝐴!E1Γ,𝑥:𝐴⊢𝑐𝑁:𝐶!E2
Γ⊢𝑐𝑀𝗍𝗈𝑥.𝑁:𝐶!(E1∪E2)
C-To^Σ
Γ⊢𝑣𝑉:𝑈E𝐶
Γ⊢𝑐𝖿𝗈𝗋𝖼𝖾𝑉:𝐶!E
C-Force^Σ
Γ,𝑥:𝐴⊢𝑐𝑀:𝐶!E
Γ⊢𝑐𝜆𝑥.𝑀:𝐴⇒E𝐶!∅
C-Lam^Σ
Γ⊢𝑐𝑀:𝐴⇒E1𝐶!E0Γ⊢𝑣𝑉:𝐴
Γ⊢𝑐𝑀𝑉:𝐶!(E0∪E1)
C-App^Σ
Σ(𝗈𝗉)=𝑃⇝𝑅Γ⊢𝑣𝑉:𝑃Γ,𝑥:𝑅⊢𝑐𝑀:𝐹𝐴!E
Γ⊢𝑐𝗈𝗉𝑉(𝑥.𝑀):𝐹𝐴!({𝗈𝗉}∪E)
C-Op
Γ⊢𝑐𝑀:𝐶!EE⊆E′
Γ⊢𝑐𝑀:𝐶!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 Σ(𝗍𝗂𝖼𝗄)=1⇝1 and abbreviate 𝑇=𝗍𝗂𝖼𝗄()(𝑢.𝗋𝖾𝗍𝗎𝗋𝗇𝑢). Extend both source translations by 𝗍𝗂𝖼𝗄()𝑣=𝑇 and 𝗍𝗂𝖼𝗄()𝑛=𝑇. For the source term (𝜆𝑥:1.())𝗍𝗂𝖼𝗄(), call by value exposes the request: 𝗋𝖾𝗍𝗎𝗋𝗇(𝗍𝗁𝗎𝗇𝗄(𝜆𝑥.𝗋𝖾𝗍𝗎𝗋𝗇()))𝗍𝗈𝑓.𝑇𝗍𝗈𝑎.(𝖿𝗈𝗋𝖼𝖾𝑓)𝑎𝑇𝑜⟶𝑇𝗍𝗈𝑎.(𝖿𝗈𝗋𝖼𝖾(𝗍𝗁𝗎𝗇𝗄(𝜆𝑥.𝗋𝖾𝗍𝗎𝗋𝗇())))𝑎. This exposed term has type 𝐹1!{𝗍𝗂𝖼𝗄}. Call by name places the request in the unused argument thunk and erases it by beta: (𝜆𝑥.𝗋𝖾𝗍𝗎𝗋𝗇())(𝗍𝗁𝗎𝗇𝗄𝑇)𝐵𝑒𝑡𝑎⟶𝗋𝖾𝗍𝗎𝗋𝗇(),⋅⊢𝑐𝗋𝖾𝗍𝗎𝗋𝗇():𝐹1!∅. Thus the two translations calculate the two observations with which the chapter began.
Let 𝐻={𝗋𝖾𝗍𝗎𝗋𝗇𝑥↦𝑁𝑟;𝗈𝗉𝑖(𝑝𝑖;𝑘𝑖)↦𝑁𝑖}𝑖∈𝐼,H=𝗁𝖺𝗇𝖽𝗅𝖾𝖽(𝐻):={𝗈𝗉𝑖∣𝑖∈𝐼}. where the operation names are distinct. Write Γ⊢ℎ𝐻:𝐴[Ein]⟹𝐵[Eout] when the following premises hold:
Ein∖H⊆Eout;
Γ,𝑥:𝐴⊢𝑐𝑁𝑟:𝐹𝐵!Eout;
for every 𝗈𝗉𝑖∈H with Σ(𝗈𝗉𝑖)=𝑃𝑖⇝𝑅𝑖, Γ,𝑝𝑖:𝑃𝑖,𝑘𝑖:𝑈∅(𝑅𝑖⇒Eout𝐹𝐵)⊢𝑐𝑁𝑖:𝐹𝐵!Eout.
The application rule for a handler is
Γ⊢𝑐𝑀:𝐹𝐴!EinΓ⊢ℎ𝐻:𝐴[Ein]⟹𝐵[Eout]
Γ⊢𝑐𝗁𝖺𝗇𝖽𝗅𝖾𝑀𝗐𝗂𝗍𝗁𝐻:𝐹𝐵!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 𝗈𝗎𝗍Ξ(𝑀), 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 {𝗈𝗉} union the effect synthesized for its continuation body. Two rules cross an annotated computation boundary. To check 𝗍𝗁𝗎𝗇𝗄𝑀 at 𝑈D𝐶, synthesize D0=𝗈𝗎𝗍Ξ(𝑀) and require D0⊆D. To check 𝜆𝑥.𝑀 at 𝐴⇒D𝐶, impose the corresponding inclusion for the body under 𝑥:𝐴. 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 𝜀ℎ to every handler occurrence ℎ in the checked term. For the operation clauses of ℎ, run the same structural recursion symbolically under 𝑘𝑖:𝑈∅(𝑅𝑖⇒𝜀ℎ𝐹𝐵). A checked term has finitely many such unknowns; call their set 𝐽. A symbolic immediate effect is a monotone map Ψ:P(Σ)𝐽⟶P(Σ). Constants, projections 𝜀ℎ, 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 𝜀ℎ=𝜀ℎ′, or a pin𝜀ℎ=D. In particular, comparing 𝑈∅(𝑅⇒𝜀ℎ𝐹𝐵) with 𝑈∅(𝑅⇒D𝐹𝐵) 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 𝜀ℎ, the inclusion is another lower-bound constraint on 𝐸ℎ and is added to Φℎ. If the expected annotation is a fixed set D, record the boundary condition Θ(¯𝐸)⊆D(𝐵) with monotone Θ. Finally, for each handler occurrence ℎ, let Ψin,ℎ, Ψ𝑟,ℎ, and Ψ𝑖,ℎ be the symbolic immediate effects of its handled computation, return clause, and operation clauses. Define Φℎ(¯𝐸):=(Ψin,ℎ(¯𝐸)∖Hℎ)∪Ψ𝑟,ℎ(¯𝐸)∪⋃𝑖∈𝐼ℎΨ𝑖,ℎ(¯𝐸).(𝐻) The constraint for ℎ is Φℎ(¯𝐸)⊆𝐸ℎ, where Φℎ also includes all symbolic boundary lower bounds whose right side is 𝜀ℎ. Every Φℎ is monotone because projections, finite unions, and difference by a fixed set are monotone.
Normalize the effect equations by quotienting 𝐽 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 𝐸𝑛+1𝑄=𝐸𝑛𝑄∪⋃ℎ∈𝑄Φℎ(¯𝐸𝑛)(𝑄unpinned).(𝐼) For one unpinned outer handler whose handled computation has already been synthesized, the first stage contains Ein∖H. 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.
Fix the annotations in Ξ, all handler result types, and a finite operation signature. For every assignment ¯𝐸 to the handler unknowns, the declarative handler premises at the outputs 𝐸ℎ have derivations if and only if the symbolic pass has no constructor clash, every generated equality and pin holds under ¯𝐸, every generated boundary condition holds under ¯𝐸, and Φℎ(¯𝐸)⊆𝐸ℎ(ℎ∈𝐽). In that case each symbolic immediate effect evaluated at ¯𝐸 is the least immediate effect of its subterm with the resumptions annotated by the corresponding components of ¯𝐸, 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 𝜀ℎ by 𝐸ℎ; 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 Θ(¯𝐸). If the expected annotation is fixed at D, the declarative premise exists exactly when Θ(¯𝐸)⊆D, which is condition (B). If it is 𝜀ℎ, the same inversion yields the lower bound Θ(¯𝐸)⊆𝐸ℎ placed in Φℎ. One C-Weaken constructs the premise when either inclusion is strict.
For a handler ℎ, use the induction hypotheses for its handled computation and every clause. Its forwarded-input premise is precisely Ψin,ℎ(¯𝐸)∖Hℎ⊆𝐸ℎ after normalization. Indeed, if the original input annotation is Ein, the induction hypothesis gives Ψin,ℎ(¯𝐸)⊆Ein; monotonicity of difference by Hℎ transports the original forwarding inclusion. Conversely, the synthesized input derivation supplies the smaller annotation Ψin,ℎ(¯𝐸) directly. The return and operation premises are precisely Ψ𝑟,ℎ(¯𝐸)⊆𝐸ℎ and Ψ𝑖,ℎ(¯𝐸)⊆𝐸ℎ. Their conjunction is Φℎ(¯𝐸)⊆𝐸ℎ. The operation-premise environment substitutes the same 𝐸ℎ for every occurrence of 𝜀ℎ, 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. ◻
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 D0⊆D. 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 ¯𝐸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 Φℎ give ¯𝐸∗⊆¯𝐸out componentwise. Each boundary expression Θ is monotone, so Θ(¯𝐸out)⊆D implies Θ(¯𝐸∗)⊆D. Thus the least assignment passes every boundary check. Constraint completeness supplies normalized clause derivations whose resumptions carry their handler components 𝐸∗,ℎ. Put one C-Weaken at a clause boundary when its synthesized immediate effect is strictly smaller than 𝐸∗,ℎ, and apply C-Handle. For the handler currently being normalized, put 𝐸∗,ℎ⊆𝐸out,ℎ 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. ◻
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 Φℎ proves that the unpinned product assignment grows componentwise in (I). If there are 𝑚 unpinned classes, the finite product lattice permits at most 𝑚|Σ| strict single-operation additions. Every declarative assignment satisfies the same pins and is a prefixed point of the unpinned system, so induction on 𝑛 puts the iterates below it. Hence ¯𝐸∗ is the least assignment compatible with the pins and handler lower bounds. If a boundary test Θ(¯𝐸∗)⊆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 Φℎ(¯𝐸∗)⊈𝐸ℎ, 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 𝐾𝐸:=𝑈∅(𝑅⇒𝐸𝐹1) and consider an operation clause whose resumption has type 𝑘:𝐾𝐸 in an environment containing 𝑔:𝑈∅(𝐾D⇒∅𝐹1). Let its body be (𝖿𝗈𝗋𝖼𝖾𝑔)𝑘, and let the return clause and the forwarded input be pure. Checking the argument of 𝖿𝗈𝗋𝖼𝖾𝑔 compares 𝐾𝐸 with 𝐾D and generates the pin 𝐸=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 𝗋𝖾𝗍𝗎𝗋𝗇(𝗍𝗁𝗎𝗇𝗄(𝗋𝖾𝗍𝗎𝗋𝗇())) to be 𝐹(𝑈D(𝐹1)). 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 𝐴 to 𝐴 with a pure fallback uses the repeated annotations Ein={𝗋𝖺𝗂𝗌𝖾}∪D,D⊆Eout,𝑘:𝑈∅(0⇒Eout𝐹𝐴),𝑁𝗋𝖺𝗂𝗌𝖾:𝐹𝐴!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 𝑆,𝖤𝗑𝖼,1,0 as base types, with ():1, 𝑒0:𝖤𝗑𝖼, and no closed constant of type 0. Also admit a fixed pure base-value update: if 𝑉:𝑆, then 𝑉+:𝑆. It denotes the same update written 𝑠+1 in the opening tree. Its complete typing rule is Γ⊢𝑣𝑉:𝑆Γ⊢𝑣𝑉+:𝑆V−Next. Put 𝑀𝗍𝗑=𝗀𝖾𝗍()(𝑠.𝗉𝗎𝗍𝑠+(𝑢.𝗋𝖺𝗂𝗌𝖾𝑒0(𝑧.𝗋𝖾𝗍𝗎𝗋𝗇𝑧))). Its successful result type is 0. The complete bottom-up derivation is the following chain of rule instances: 𝑠:𝑆,𝑢:1,𝑧:0⊢𝑣𝑧:0,𝑉−𝑉𝑎𝑟𝑠:𝑆,𝑢:1,𝑧:0⊢𝑐𝗋𝖾𝗍𝗎𝗋𝗇𝑧:𝐹0!∅,𝐶−𝑅𝑒𝑡𝑢𝑟𝑛Σ𝑠:𝑆,𝑢:1⊢𝑐𝗋𝖺𝗂𝗌𝖾𝑒0(𝑧.𝗋𝖾𝗍𝗎𝗋𝗇𝑧):𝐹0!{𝗋𝖺𝗂𝗌𝖾},𝐶−𝑂𝑝𝑠:𝑆⊢𝑣𝑠+:𝑆,𝑉−𝑁𝑒𝑥𝑡𝑠:𝑆⊢𝑐𝗉𝗎𝗍𝑠+(𝑢.𝗋𝖺𝗂𝗌𝖾𝑒0(𝑧.𝗋𝖾𝗍𝗎𝗋𝗇𝑧)):𝐹0!E𝑝𝑟,𝐶−𝑂𝑝⋅⊢𝑐𝑀𝗍𝗑:𝐹0!E𝑔𝑝𝑟.𝐶−𝑂𝑝 Here E𝑝𝑟={𝗉𝗎𝗍,𝗋𝖺𝗂𝗌𝖾} and E𝑔𝑝𝑟={𝗀𝖾𝗍,𝗉𝗎𝗍,𝗋𝖺𝗂𝗌𝖾}. 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 𝐻𝖺𝖻𝗈𝗋𝗍 have return clause 𝑥↦𝗋𝖾𝗍𝗎𝗋𝗇() and the single operation clause 𝗋𝖺𝗂𝗌𝖾(𝑝;𝑘)↦𝗋𝖾𝗍𝗎𝗋𝗇(). It discards the successful value and catches an exception, but forwards state operations. With E𝑠={𝗀𝖾𝗍,𝗉𝗎𝗍}, the complete handler instance is ⋅⊢𝑐𝑀𝗍𝗑:𝐹0!E𝑔𝑝𝑟E𝑔𝑝𝑟∖{𝗋𝖺𝗂𝗌𝖾}⊆E𝑠𝑥:0⊢𝑐𝗋𝖾𝗍𝗎𝗋𝗇():𝐹1!E𝑠𝑝:𝖤𝗑𝖼,𝑘:𝑈∅(0⇒E𝑠𝐹1)⊢𝑐𝗋𝖾𝗍𝗎𝗋𝗇():𝐹1!E𝑠⋅⊢ℎ𝐻𝖺𝖻𝗈𝗋𝗍:0[E𝑔𝑝𝑟]⟹1[E𝑠]Handler⋅⊢𝑐𝗁𝖺𝗇𝖽𝗅𝖾𝑀𝗍𝗑𝗐𝗂𝗍𝗁𝐻𝖺𝖻𝗈𝗋𝗍:𝐹1!E𝑠C−Handle. The two clause-body premises use C-ReturnΣ followed by C-Weaken; the set-difference premise is equality.
★☆☆ Derive the type of 𝗍𝗁𝗎𝗇𝗄(𝜆𝑥.𝗋𝖺𝗂𝗌𝖾𝑒0(𝑧.𝗋𝖾𝗍𝗎𝗋𝗇𝑧)), giving 𝑥 the value type 1 and 𝑒0 the exception type 𝖤𝗑𝖼. Distinguish the thunk’s immediate effect, the arrow’s immediate effect, and the arrow’s latent effect.
★☆☆ Write all premises for a handler of 𝖼𝗁𝗈𝗈𝗌𝖾 which resumes first with false, discards that result, and then resumes with true. Which occurrence of the output effect set types each resumption?
The old evaluation frames are extended by 𝗁𝖺𝗇𝖽𝗅𝖾𝐾𝗐𝗂𝗍𝗁𝐻. For a fixed operation 𝗈𝗉, a handler-free 𝗈𝗉-open context is generated by 𝑋𝗈𝗉::=[]∣𝑋𝗈𝗉𝗍𝗈𝑥.𝑁. There is no application frame: an exposed request has type 𝐹𝐴, not a function computation type. Because 𝑋𝗈𝗉 contains no handler, an intervening handler without an 𝗈𝗉-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 𝐻0 has no 𝗈𝗉-clause, then 𝗁𝖺𝗇𝖽𝗅𝖾(𝗈𝗉𝑉(𝑥.𝑁))𝗐𝗂𝗍𝗁𝐻0 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 𝑋𝗈𝗉. The convention is separate from the trace length ℓ, which was defined only for 𝖢𝖡𝖯𝖵0.
Suppose 𝐻={𝗋𝖾𝗍𝗎𝗋𝗇𝑥↦𝑁𝑟;…;𝗈𝗉(𝑝;𝑘)↦𝑁𝗈𝗉;…}. The two handling reductions are Handle-Return, 𝗁𝖺𝗇𝖽𝗅𝖾(𝗋𝖾𝗍𝗎𝗋𝗇𝑉)𝗐𝗂𝗍𝗁𝐻⟶𝑁𝑟[𝑉/𝑥], and Handle-Op, 𝗁𝖺𝗇𝖽𝗅𝖾𝑋𝗈𝗉[𝗈𝗉𝑉(𝑥.𝑀)]𝗐𝗂𝗍𝗁𝐻⟶𝑁𝗈𝗉[𝑉/𝑝,̂𝑘/𝑘], where ̂𝑘=𝗍𝗁𝗎𝗇𝗄(𝜆𝑦.𝗁𝖺𝗇𝖽𝗅𝖾𝑋𝗈𝗉[𝑀[𝑦/𝑥]]𝗐𝗂𝗍𝗁𝐻). The same 𝐻 occurs inside ̂𝑘. A call (𝖿𝗈𝗋𝖼𝖾𝑘)𝑊 therefore resumes under the same handler. This is the defining difference between a deep handler and a shallow one. A shallow variant substitutes instead ̂𝑘𝗌𝗁𝖺𝗅𝗅𝗈𝗐=𝗍𝗁𝗎𝗇𝗄(𝜆𝑦.𝑋𝗈𝗉[𝑀[𝑦/𝑥]]), so operations exposed after resumption escape this occurrence of 𝐻.
If 𝗈𝗉∉𝗁𝖺𝗇𝖽𝗅𝖾𝖽(𝐻), the handler forwards the request: 𝗁𝖺𝗇𝖽𝗅𝖾𝑋𝗈𝗉[𝗈𝗉𝑉(𝑥.𝑀)]𝗐𝗂𝗍𝗁𝐻⟶Handle−Forward𝗈𝗉𝑉(𝑦.𝗁𝖺𝗇𝖽𝗅𝖾𝑋𝗈𝗉[𝑀[𝑦/𝑥]]𝗐𝗂𝗍𝗁𝐻). 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 𝗀𝖾𝗍 with 𝑠0 and resumes 𝗉𝗎𝗍 with (), the selected roots are 𝗁𝖺𝗇𝖽𝗅𝖾𝑀𝗍𝗑𝗐𝗂𝗍𝗁𝐻𝖺𝖻𝗈𝗋𝗍𝐻𝑎𝑛𝑑𝑙𝑒−𝐹𝑜𝑟𝑤𝑎𝑟𝑑⟶𝗀𝖾𝗍()(𝑠.𝗁𝖺𝗇𝖽𝗅𝖾𝗉𝗎𝗍𝑠+(𝑢.𝗋𝖺𝗂𝗌𝖾𝑒0(𝑧.𝗋𝖾𝗍𝗎𝗋𝗇𝑧))𝗐𝗂𝗍𝗁𝐻𝖺𝖻𝗈𝗋𝗍),𝗁𝖺𝗇𝖽𝗅𝖾𝗉𝗎𝗍𝑠+0(𝑢.𝗋𝖺𝗂𝗌𝖾𝑒0(𝑧.𝗋𝖾𝗍𝗎𝗋𝗇𝑧))𝗐𝗂𝗍𝗁𝐻𝖺𝖻𝗈𝗋𝗍𝐻𝑎𝑛𝑑𝑙𝑒−𝐹𝑜𝑟𝑤𝑎𝑟𝑑⟶𝗉𝗎𝗍𝑠+0(𝑢.𝗁𝖺𝗇𝖽𝗅𝖾𝗋𝖺𝗂𝗌𝖾𝑒0(𝑧.𝗋𝖾𝗍𝗎𝗋𝗇𝑧)𝗐𝗂𝗍𝗁𝐻𝖺𝖻𝗈𝗋𝗍),𝗁𝖺𝗇𝖽𝗅𝖾𝗋𝖺𝗂𝗌𝖾𝑒0(𝑧.𝗋𝖾𝗍𝗎𝗋𝗇𝑧)𝗐𝗂𝗍𝗁𝐻𝖺𝖻𝗈𝗋𝗍𝐻𝑎𝑛𝑑𝑙𝑒−𝑂𝑝⟶𝗋𝖾𝗍𝗎𝗋𝗇(). The write is forwarded before the exception is caught; this handler therefore does not roll state back.
The operation case calculated
Assume Σ(𝖼𝗁𝗈𝗈𝗌𝖾)=1⇝𝖡𝗈𝗈𝗅 and let 𝐻𝗍𝗐𝗂𝖼𝖾 have return clause 𝑥↦𝗋𝖾𝗍𝗎𝗋𝗇𝑥 and choose clause 𝖼𝗁𝗈𝗈𝗌𝖾(𝑝;𝑘)↦(𝖿𝗈𝗋𝖼𝖾𝑘)𝖿𝖺𝗅𝗌𝖾𝗍𝗈_.(𝖿𝗈𝗋𝖼𝖾𝑘)𝗍𝗋𝗎𝖾. The first result is deliberately discarded; the second is returned. For 𝑀=𝖼𝗁𝗈𝗈𝗌𝖾()(𝑏.𝗋𝖾𝗍𝗎𝗋𝗇𝑏), the handler step substitutes ̂𝑘=𝗍𝗁𝗎𝗇𝗄(𝜆𝑏.𝗁𝖺𝗇𝖽𝗅𝖾(𝗋𝖾𝗍𝗎𝗋𝗇𝑏)𝗐𝗂𝗍𝗁𝐻𝗍𝗐𝗂𝖼𝖾). The two resumptions can be read directly from the annotated calculation 𝗁𝖺𝗇𝖽𝗅𝖾𝑀𝗐𝗂𝗍𝗁𝐻𝗍𝗐𝗂𝖼𝖾𝐻𝑎𝑛𝑑𝑙𝑒−𝑂𝑝⟶(𝖿𝗈𝗋𝖼𝖾̂𝑘)𝖿𝖺𝗅𝗌𝖾𝗍𝗈_.(𝖿𝗈𝗋𝖼𝖾̂𝑘)𝗍𝗋𝗎𝖾𝐹𝑜𝑟𝑐𝑒⟶(𝜆𝑏.𝗁𝖺𝗇𝖽𝗅𝖾(𝗋𝖾𝗍𝗎𝗋𝗇𝑏)𝗐𝗂𝗍𝗁𝐻𝗍𝗐𝗂𝖼𝖾)𝖿𝖺𝗅𝗌𝖾𝗍𝗈_.(𝖿𝗈𝗋𝖼𝖾̂𝑘)𝗍𝗋𝗎𝖾𝐵𝑒𝑡𝑎⟶𝗁𝖺𝗇𝖽𝗅𝖾(𝗋𝖾𝗍𝗎𝗋𝗇𝖿𝖺𝗅𝗌𝖾)𝗐𝗂𝗍𝗁𝐻𝗍𝗐𝗂𝖼𝖾𝗍𝗈_.(𝖿𝗈𝗋𝖼𝖾̂𝑘)𝗍𝗋𝗎𝖾𝐻𝑎𝑛𝑑𝑙𝑒−𝑅𝑒𝑡𝑢𝑟𝑛⟶𝗋𝖾𝗍𝗎𝗋𝗇𝖿𝖺𝗅𝗌𝖾𝗍𝗈_.(𝖿𝗈𝗋𝖼𝖾̂𝑘)𝗍𝗋𝗎𝖾𝑇𝑜⟶(𝖿𝗈𝗋𝖼𝖾̂𝑘)𝗍𝗋𝗎𝖾𝐹𝑜𝑟𝑐𝑒,𝐵𝑒𝑡𝑎,𝐻𝑎𝑛𝑑𝑙𝑒−𝑅𝑒𝑡𝑢𝑟𝑛⟶∗𝗋𝖾𝗍𝗎𝗋𝗇𝗍𝗋𝗎𝖾. 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: 𝑘 is an ordinary thunked value, so 𝐻𝗍𝗐𝗂𝖼𝖾 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 ̂𝑘 instead retains or copies the captured 𝑋𝗈𝗉, with cost proportional to that context unless it is shared persistently. Thus 𝐻𝗍𝗐𝗂𝖼𝖾 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 𝑀𝗍𝗑 belongs to the first-order reification fragment of section 22.9, as does the 𝐻𝗍𝗐𝗂𝖼𝖾 clause once its two calls are written with the abbreviation 𝗋𝖾𝗌𝗎𝗆𝖾𝑘𝑉:=(𝖿𝗈𝗋𝖼𝖾𝑘)𝑉. 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.
The annotated value, computation, and handler judgments admit weakening and value substitution. In addition, let a derivation of Γ⊢𝑐𝑋[𝑀]:𝐶!E contain at its hole a subderivation Δ⊢𝑐𝑀:𝐷!E0. If Δ⊢𝑐𝑀′:𝐷!E0, then replacing that occurrence gives Γ⊢𝑐𝑋[𝑀′]:𝐶!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. ◻
Proof of Lemma 22.24 — Typed operation decomposition
Proof. Induct outward through 𝑋𝗈𝗉. At the hole, C-Op contributes 𝗈𝗉 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 𝑋𝗈𝗉. Inversion at C-Op gives Γ⊢𝑣𝑉:𝑃𝗈𝗉 and Γ,𝑥:𝑅𝗈𝗉⊢𝑐𝑀:𝐹𝐴!E0 for its continuation result type and effect. ◻
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 Γ,𝑥:𝐴⊢𝑐𝑁𝑟:𝐹𝐵!Eout and Γ⊢𝑣𝑉:𝐴. Substitution types 𝑁𝑟[𝑉/𝑥].
For a handled operation, invert the typed decomposition of the exposed request inside 𝑋𝗈𝗉. For a fresh response variable 𝑦:𝑅𝗈𝗉, response substitution first gives 𝑀[𝑦/𝑥]:𝐹𝐴!E0. Rule C-Weaken enlarges this annotation to {𝗈𝗉}∪E0, exactly the annotation of the operation hole. Replacement now types the continuation 𝑋𝗈𝗉[𝑀[𝑦/𝑥]] at the handled input type. Rule C-Handle then gives Γ,𝑦:𝑅𝗈𝗉⊢𝑐𝗁𝖺𝗇𝖽𝗅𝖾𝑋𝗈𝗉[𝑀[𝑦/𝑥]]𝗐𝗂𝗍𝗁𝐻:𝐹𝐵!Eout. Rules C-LamΣ and V-ThunkΣ type ̂𝑘 as 𝑈∅(𝑅𝗈𝗉⇒Eout𝐹𝐵). 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. ◻
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 𝐹𝐴. The operation membership assertion is lemma 22.24. ◻
Proof. Preservation keeps the empty effect annotation. The third progress outcome would require 𝗈𝗉∈∅. Hence a finite maximal reduction can stop only at a terminal computation, which preservation keeps at type 𝐶. ◻
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.
★☆☆ Show that replacing the handler side condition Ein∖H⊆Eout by Ein⊆Eout remains safe but fails to express elimination. Give the inferred-looking output of an exception handler under that bad rule.
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 𝐿::=𝗋𝖾𝗍𝗎𝗋𝗇𝑉∣𝗈𝗉𝑉(𝑥.𝐿)∣𝐿𝗍𝗈𝑥.𝐿∣𝗋𝖾𝗌𝗎𝗆𝖾𝑘𝑉∣𝗁𝖺𝗇𝖽𝗅𝖾𝐿𝗐𝗂𝗍𝗁𝐻. A handler clause is a term 𝐿 whose only free variables are its displayed base parameter and, in an operation clause, its continuation variable 𝑘. The notation 𝗋𝖾𝗌𝗎𝗆𝖾𝑘𝑉 elaborates to (𝖿𝗈𝗋𝖼𝖾𝑘)𝑉. There are no other lambdas, thunks, forces, or applications in this fragment.
A handler step replaces 𝑘 by a thunked lambda. Its reduct therefore lies in the administrative closure of the grammar: besides the terms above, this closure admits exactly (𝖿𝗈𝗋𝖼𝖾(𝗍𝗁𝗎𝗇𝗄(𝜆𝑦.𝑃)))𝑉and(𝜆𝑦.𝑃)𝑉 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 𝑃[𝑉/𝑦]. 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 [[𝑥]]𝜌=𝜌(𝑥),[[𝑐]]𝜌=𝑐 for variables and closed base constants, and define [[𝗋𝖾𝗍𝗎𝗋𝗇𝑉]]𝜌,𝜅=𝖱𝖾𝗍([[𝑉]]𝜌),[[𝗈𝗉𝑉(𝑥.𝐿)]]𝜌,𝜅=𝖮𝗉𝗈𝗉([[𝑉]]𝜌,𝜆𝑟.[[𝐿]]𝜌[𝑥↦𝑟],𝜅),[[𝐿1𝗍𝗈𝑥.𝐿2]]𝜌,𝜅=[[𝐿1]]𝜌,𝜅≫=(𝜆𝑎.[[𝐿2]]𝜌[𝑥↦𝑎],𝜅),[[𝗋𝖾𝗌𝗎𝗆𝖾𝑘𝑉]]𝜌,𝜅=𝜅(𝑘)([[𝑉]]𝜌). For a handler 𝐻, its algebra is 𝑟𝐻(𝑎)=[[𝑁𝑟]][𝑥↦𝑎],∅,ℎ𝐻,𝗈𝗉(𝑝,𝑔)=[[𝑁𝗈𝗉]][𝑝𝗈𝗉↦𝑝],[𝑘𝗈𝗉↦𝑔](𝗈𝗉∈𝗁𝖺𝗇𝖽𝗅𝖾𝖽(𝐻)),ℎ𝐻,𝗈𝗉(𝑝,𝑔)=𝖮𝗉𝗈𝗉(𝑝,𝑔)(𝗈𝗉∉𝗁𝖺𝗇𝖽𝗅𝖾𝖽(𝐻)). The clauses are closed apart from the indicated binders, so no omitted environment is needed. Finally define [[𝗁𝖺𝗇𝖽𝗅𝖾𝐿𝗐𝗂𝗍𝗁𝐻]]𝜌,𝜅=𝖿𝗈𝗅𝖽𝑟𝐻,ℎ𝐻([[𝐿]]𝜌,𝜅). These equations are structural definitions on the displayed fragment; none appeals to an operational handler step.
For this section an 𝗈𝗉-open first-order context is 𝑋𝖿𝗈𝗈𝗉::=[]∣𝑋𝖿𝗈𝗈𝗉𝗍𝗈𝑥.𝐿∣𝗁𝖺𝗇𝖽𝗅𝖾𝑋𝖿𝗈𝗈𝗉𝗐𝗂𝗍𝗁𝐺,𝗈𝗉∉𝗁𝖺𝗇𝖽𝗅𝖾𝖽(𝐺). In particular, it has no general application frame. The only applications admitted above are administrative resumptions.
For every 𝗈𝗉-open first-order context 𝑋𝖿𝗈𝗈𝗉, after alpha-renaming the response variable 𝑥 fresh for that context, [[𝑋𝖿𝗈𝗈𝗉[𝗈𝗉𝑉(𝑥.𝐿)]]]𝜌,𝜅=𝖮𝗉𝗈𝗉([[𝑉]]𝜌,𝜆𝑟.[[𝑋𝖿𝗈𝗈𝗉[𝐿]]]𝜌[𝑥↦𝑟],𝜅).
Proof of Lemma 22.29 — Reification through an open context
Proof. Write 𝑋 for the displayed 𝑋𝖿𝗈𝗈𝗉 inside this proof and induct on it. The empty context is the operation clause of reification. For 𝑋=𝑋′𝗍𝗈𝑧.𝑁, apply the induction hypothesis and then the operation clause for bind: 𝖮𝗉(𝑝,𝑔)≫=𝑓=𝖮𝗉(𝑝,𝜆𝑟.𝑔(𝑟)≫=𝑓). The right side is the required reification of 𝑋′[𝐿]𝗍𝗈𝑧.𝑁. For 𝑋=𝗁𝖺𝗇𝖽𝗅𝖾𝑋′𝗐𝗂𝗍𝗁𝐺, the induction hypothesis rewrites the reification of 𝑋′[𝗈𝗉𝑉(𝑥.𝐿)] to 𝖮𝗉𝗈𝗉(𝑝,𝑔) with the parameter and branch function from the lemma statement. Since 𝐺 has no clause for 𝗈𝗉, its algebra sends (𝑝,𝑔) to 𝖮𝗉𝗈𝗉(𝑝,𝑔). 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 𝑘, whereas the algebra reads 𝑘 from an environment. Lemma 22.30 proves equality of the two reifications.
Let ̂𝑘=𝗍𝗁𝗎𝗇𝗄(𝜆𝑦.𝗁𝖺𝗇𝖽𝗅𝖾𝑋[𝐿[𝑦/𝑥]]𝗐𝗂𝗍𝗁𝐻) where 𝑥 is fresh for 𝑋, and put 𝑔(𝑟)=[[𝗁𝖺𝗇𝖽𝗅𝖾𝑋[𝐿]𝗐𝗂𝗍𝗁𝐻]]𝜌[𝑥↦𝑟],𝜅. For every first-order clause body 𝑁, [[𝑁[̂𝑘/𝑘]]]𝜌,𝜅=[[𝑁]]𝜌,𝜅[𝑘↦𝑔], where the left side uses the two administrative reification clauses above. Base-value substitution likewise satisfies [[𝑁[𝑉/𝑧]]]𝜌,𝜅=[[𝑁]]𝜌[𝑧↦[[𝑉]]𝜌],𝜅.
Proof of Lemma 22.30 — Administrative reification of a deep continuation
Proof. Both equations are inductions on 𝑁. 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 𝑅(𝑦):=𝗁𝖺𝗇𝖽𝗅𝖾𝑋[𝐿[𝑦/𝑥]]𝗐𝗂𝗍𝗁𝐻: (𝖿𝗈𝗋𝖼𝖾̂𝑘)𝑉reifiesas(𝜆𝑦.𝑅(𝑦))𝑉reifiesas𝑅(𝑉). Its reification is 𝑔([[𝑉]]𝜌) by base-value substitution, exactly the meaning assigned to 𝗋𝖾𝗌𝗎𝗆𝖾𝑘𝑉 by 𝜅[𝑘↦𝑔]. ◻
For a closed term, write [[𝑃]] for its denotation at the unique empty value and continuation environments.
Let 𝑃 and 𝑃′ be closed, well-typed terms in the administrative closure of the first-order fragment. If 𝑃⟶𝑃′ is one of the following reductions, then [[𝑃]]=[[𝑃′]].
a return, handled-operation, or forwarding handler step;
a sequencing step (𝗋𝖾𝗍𝗎𝗋𝗇𝑉)𝗍𝗈𝑥.𝑁⟶𝑁[𝑉/𝑥];
either administrative continuation step displayed above;
a compatible step in a first-order sequencing or handler frame.
Consequently, if 𝑀 is a closed first-order computation and 𝗁𝖺𝗇𝖽𝗅𝖾𝑀𝗐𝗂𝗍𝗁𝐻⟶∗𝑄 uses only these reductions and 𝑄 is a handler-free first-order computation, then [[𝑄]]=𝖿𝗈𝗅𝖽𝑟𝐻,ℎ𝐻([[𝑀]]).
Proof. For a return-handler step, base-value substitution gives [[𝑁𝑟[𝑉/𝑥]]]=𝑟𝐻([[𝑉]]), 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 ℎ𝐻,𝗈𝗉(𝑝,𝜆𝑟.[[𝗁𝖺𝗇𝖽𝗅𝖾𝑋[𝐿]𝗐𝗂𝗍𝗁𝐻]]𝜌[𝑥↦𝑟],𝜅). 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 𝖮𝗉𝗈𝗉(𝑝,𝜆𝑟.[[𝗁𝖺𝗇𝖽𝗅𝖾𝑋[𝐿]𝗐𝗂𝗍𝗁𝐻]]𝜌[𝑥↦𝑟],𝜅), which proves the forwarding case.
For sequencing, reification turns the left side into 𝖱𝖾𝗍(𝑎)≫=𝑓=𝑓(𝑎), 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 [[𝗁𝖺𝗇𝖽𝗅𝖾𝑀𝗐𝗂𝗍𝗁𝐻]]=[[𝑄]]. 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 𝗋𝖾𝗌𝖾𝗍(𝐸[𝗌𝗁𝗂𝖿𝗍𝑘.𝑀])⟶𝗋𝖾𝗌𝖾𝗍(𝑀[(𝜆𝑥.𝗋𝖾𝗌𝖾𝗍(𝐸[𝑥]))/𝑘]). For this boundary calculation only, evaluation also has ordinary call-by-value beta reduction, arithmetic on numerals, and 𝗋𝖾𝗌𝖾𝗍(𝑛)⟶𝑛 for a numeral 𝑛. Let 𝑣 range over numerals and lambda abstractions. Delimiter-free capture contexts and full evaluation contexts are 𝐸::=[]∣𝐸𝑀∣𝑣𝐸∣𝐸+𝑀∣𝑛+𝐸∣𝐸×𝑀∣𝑛×𝐸,𝐷::=[]∣𝐷𝑀∣𝑣𝐷∣𝐷+𝑀∣𝑛+𝐷∣𝐷×𝑀∣𝑛×𝐷∣𝗋𝖾𝗌𝖾𝗍(𝐷). Thus 𝐸 cannot cross a reset, while 𝐷 lets ordinary root reductions run beneath a delimiter. Besides the displayed shift and reset roots, use (𝜆𝑥.𝑀)𝑣⟶𝑀[𝑣/𝑥], the usual numeral arithmetic roots, and the frame rule 𝑀⟶𝑀′𝐷[𝑀]⟶𝐷[𝑀′]. 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 𝐸=1+[] and a body which ignores 𝑘. Then 𝗋𝖾𝗌𝖾𝗍(1+𝗌𝗁𝗂𝖿𝗍𝑘.0)⟶∗0, whereas moving the context into the body gives 𝗋𝖾𝗌𝖾𝗍(𝗌𝗁𝗂𝖿𝗍𝑘.(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 𝗌𝗁𝗂𝖿𝗍 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 𝗍𝗐𝗂𝖼𝖾 which applies an effectful callback twice. Its useful CBPV type has the schematic form 𝑈∅(𝐴⇒𝜀𝐹𝐴)⇒∅(𝐴⇒𝜀𝐹𝐴). 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 ∀𝜀.𝑈{𝗀𝖾𝗍,𝗉𝗎𝗍}∪𝜀(𝐹𝐴)⇒𝜀(𝑆⇒𝜀𝐹𝐴), 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.
★☆☆ Choose three concrete callback effect sets and write three separate types for 𝗍𝗐𝗂𝖼𝖾 in the closed-set grammar. Identify the repeated pieces which a quantified effect variable should abstract.
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.
★★☆ Repeat both transaction calculations with a successful return after 𝗉𝗎𝗍. Then move the raise before the write. Display the two handler orders for each modified program and state which of these four outcomes differ.
★★☆ Delete the handler around 𝑋𝗈𝗉[𝑀[𝑦/𝑥]] in ̂𝑘. Evaluate 𝖼𝗁𝗈𝗈𝗌𝖾()(𝑏1.𝖼𝗁𝗈𝗈𝗌𝖾()(𝑏2.𝗋𝖾𝗍𝗎𝗋𝗇𝑏2)) under 𝐻𝗍𝗐𝗂𝖼𝖾. Identify the first request which becomes exposed and the exact missing handler occurrence that would capture it.
★☆☆ 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 𝗀𝖾𝗍 clause. Display the rebuilt continuation and the nearest handler which captures the request.
★★☆ Reconstruct the handled-operation case of preservation for 𝖼𝗁𝗈𝗈𝗌𝖾, including the type of ̂𝑘. Mark response substitution, effect weakening, replacement, parameter substitution, and continuation substitution separately.
★★☆ Reify the complete 𝐻𝗍𝗐𝗂𝖼𝖾 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.
★★☆ Use exactly the boundary rules displayed in section 22.10 and 𝐸=2×[]. Compare the original and context-pushed terms first with body 𝑘3 and then with body 𝑘(𝑘3). Calculate all four normal forms. Identify the equality in the one-invocation case and the failure caused by invoking the captured doubling context twice.
★★☆ Let 𝐻 be a pure handler for 𝗈𝗉, and compare 𝗁𝖺𝗇𝖽𝗅𝖾(𝗈𝗉()(𝑥.𝗋𝖾𝗍𝗎𝗋𝗇𝑥))𝗐𝗂𝗍𝗁𝐻 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 {𝗈𝗉}, its result has ∅, and the outer handler therefore receives ∅. Then represent one and two available scoped layers by the proposed set annotations {𝗈𝗉} and {𝗈𝗉}∪{𝗈𝗉}. Prove that they are equal and state the one-occurrence removal question that this annotation alone cannot answer.
★★★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 𝗉𝗎𝗍, and discard a forwarded 𝗀𝖾𝗍. 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.
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].