Lectures onType Theory
Chapter 30
Chapter 30Optional

Higher-Order Algebraic Effects and Modular Elaboration

Prerequisites. Direct starred prerequisites: none. No later core chapter depends on this route.

A first-order operation node contains an operation and the continuation which receives its response. This is enough for reading state, raising an exception, or choosing a Boolean. It is not enough for catch(M1,M2), because both arguments are computations. An implementation of catch must inspect the completed translation of M1, and it must retain the completed translation of M2 for the failure case. A first-order fold recursively translates only the response continuation. Neither computation argument has a place in which that recursion can occur.

One repair is a global translation that pattern matches on every source constructor, but then adding an operation requires editing the translator. A modular representation instead associates a translation clause with each operation and composes independently defined clauses.

Two recursive operations are distinct. For a higher-order node with fork family ψ and continuation k, monadic bind changes only k(r) to k(r)=f. Elaboration applies its recursive map to every ψ(s) and every k(r). Hence bind recursion ranges over responses, while elaboration recursion ranges over forks and responses.

The missing recursive positions

Recall the first-order signature used in chapter 22. It is a pair Δ=(OpΔ,RetΔ),RetΔ:OpΔSet. Its free tree is generated by pure(a):FreeΔ(A),a:A,impure(o,k):FreeΔ(A),o:OpΔ,k:RetΔ(o)FreeΔ(A). For a family G:SetSet, a generator g:AG(A), and an ordinary algebra ao:(RetΔ(o)G(A))G(A), the fold equations are foldg,a(pure(x))=g(x),foldg,a(impure(o,k))=ao(foldg,ak). The recursive calls in the second line are indexed by responses to o. There is no other recursive position.

Suppose we try to add a catch operation at result type A. The most direct first-order arity would be catchA:(FreeΔ(A)×FreeΔ(A))A. This is not an ordinary operation signature of the form (24.1). Its parameter type mentions both the ambient target signature Δ and the result type A. Worse, even if the pair in (24.1) were admitted as an opaque parameter, the fold in (24.1) would receive the two trees without recursively folded results. The catch clause would then have to invoke the global translator itself.

A second attempt stores source syntax rather than target trees: catchA:(SyntaxH(A)×SyntaxH(A))A. Now the signature mentions the entire source signature H, and every target algebra which handles catch must know how to translate arbitrary SyntaxH-trees. The operation-specific clause is no longer modular. Equations (24.1) and (24.1) expose the same defect from opposite sides: the recursively translated computation arguments are absent from the first-order node interface.

Proposition 24.1 — First-order fold obstruction

Call an algebra clause local to its interface when its only recursive inputs are the folded response family of (24.1). Such a clause receives neither source syntax nor a recursive translator. Let an ordinary free-tree fold have exactly that interface. Consider a constructor with a designated computation argument M:SyntaxH(B). Suppose that M is not a response branch k(r). The fold cannot pass the recursively folded value of M to its algebra clause unless either

  1. M is placed in the response continuation of the constructor, changing its binding and sequencing role; or

  2. the algebra clause is given an additional recursive translator for the whole source signature.

Thus no interface-local clause for (24.1) supports modular elaboration of such a constructor.

Proof of Proposition 24.1 — First-order fold obstruction

Proof. The fold’s operation clause receives only o and the family foldg,ak. Every recursive result available to the algebra is therefore the image of some response branch k(r). If M is not one of these branches, its folded value is absent. Putting M into k makes it a computation selected by an operation response and followed by the surrounding continuation; that is not the role of an independently scoped argument. Otherwise the only way for the clause to obtain a folded value of M is to call a translator not supplied by (24.1). Such a translator is indexed by the whole source signature, so the clause ceases to be modular. ◻

Exercise 24.1 — The opaque-parameter failure

★☆☆ Assume (24.1) is nevertheless admitted as an ordinary operation parameter. Write the type of the catch component of a fold algebra FreeΔ(A)G(A). Show that the component receives untranslated source computations and identify the additional argument it would need in order to translate them. Explain why that argument mentions the whole source signature rather than only catch.

Higher-order signatures

The missing information is not an untyped list of child trees. Different computation arguments may return different types, and their number and result types may depend on the operation. We therefore index the recursive positions by an ordinary effect signature.

Write Effect for the class of small ordinary signatures Δ=(OpΔ,RetΔ), where OpΔ:Set and RetΔ:OpΔSet. Thus ForkH(o):Effect below is an ordinary signature, not an effectful computation or an additional universe of terms.

Definition 24.2 — Higher-order effect signature

A higher-order effect signature is a triple H=(OpHH,ForkH,RetHH) with OpHH:Set,ForkH:OpHHEffect,RetHH:OpHHSet. For o:OpHH, the ordinary signature ForkH(o) indexes the computation arguments of o. A fork index s:OpForkH(o) names one such argument, whose result type is RetForkH(o)(s). The set RetHH(o) is the response type supplied to the ordinary continuation after the higher-order operation has completed.

The word fork refers to the family of nested computations; it is not a parallel-evaluation claim. The signature specifies their shape, not an execution order.

Higher-order signatures have a disjoint sum. We write H1H2 for the signature defined by OpH1H2H=OpH1H+OpH2H,ForkH1H2(inl(o))=ForkH1(o),ForkH1H2(inr(o))=ForkH2(o),RetH1H2H(inl(o))=RetH1H(o),RetH1H2H(inr(o))=RetH2H(o). The symbol is used only for this higher-order signature sum. For ordinary signatures Δi=(OpΔi,RetΔi), define their disjoint sum Δ1sigΔ2 by OpΔ1sigΔ2=OpΔ1+OpΔ2,RetΔ1sigΔ2(inlo)=RetΔ1(o),RetΔ1sigΔ2(inro)=RetΔ2(o). Thus sig composes first-order target operations, whereas also composes their fork signatures. Unparenthesized chains of either signature-sum symbol associate to the right; this parsing convention does not make the two nestings definitionally equal.

Definition 24.3 — Hefty tree

For a higher-order signature H and a set A, the set of hefty trees HeftyH(A) is generated by pure(a):HeftyH(A),a:A,impure(o,ψ,k):HeftyH(A), where the second constructor has data o:OpHH,ψ:s:OpForkH(o)HeftyH(RetForkH(o)(s)),k:RetHH(o)HeftyH(A). We call ψ the fork, the family of computation arguments, and k the continuation that receives the operation result.

The word “generated” denotes the least indexed family closed under these two constructors. Equivalently, it is the W-type of the displayed set-indexed polynomial signature; the ambient set theory is assumed to admit these W-types. Its induction principle is simultaneous in the result index. For predicates PA(M) at every set A, it is enough to prove pure:PA(pure(a));impure:(s.PRetForkH(o)(s)(ψ(s)))(r.PA(k(r)))PA(impure(o,ψ,k)). Unlike the nested scoped syntax of definition 23.2, this is a direct indexed polynomial W-type; it needs no diagonal transfinite-chain hypothesis.

The two function fields in (24.3) play different roles. The family ψ contains computations owned by the operation. The function k contains the computation which follows the operation’s result. This distinction will control bind.

Catch as a typed fork

To make operations polymorphic in their result type, fix a small universe of codes Ty with interpretation Val:TySet. No closure property of this universe is used below; it may contain only the codes required by the examples.

Definition 24.4 — The catch signature

The signature Catch has one operation catch(t) for each t:Ty, with ForkCatch(catch(t))=(2,λb.Val(t)),RetCatchH(catch(t))=Val(t). Thus the fork has two indices, tt and ff, and both indexed computations return a value of type Val(t).

First take the head-summand row H=CatchH. For M1,M2:HeftyH(Val(t)), define catch(M1,M2):=impure(ι(catch(t)),λb.if b then M1 else M2,pure). The fork stores the protected and fallback computations. The continuation is pure because the operation itself returns whichever branch value is selected. A surrounding bind will replace this continuation without altering either branch.

Example 24.5 — The three type indices of a catch node

Let t code N, let M1,M2:HeftyH(N), and let the whole surrounding computation return B. In a node impure(catch(t),ψ,k):HeftyH(B), one has ψ(tt),ψ(ff):HeftyH(N),k:NHeftyH(B). The branch type is fixed by the code carried by the operation; the final type B is fixed by the surrounding continuation. Conflating these indices would reject ordinary sequencing after catch.

The container shape (24.3) is deliberately specific. It represents a family of strictly positive recursive positions indexed by an ordinary signature. A semantically describable higher-order operator whose syntax places the recursive family negatively is not admitted merely because one can write a set-theoretic endofunctor for it.

Exercise 24.2 — A three-branch operation

★☆☆ Define a higher-order signature First with one operation first(t) carrying three computations of type Val(t) and returning a value of type Val(t). Give its fork signature and write the analogue of (24.2.1) for first(M0,M1,M2).

Sequencing is not structural recursion

For an ordinary free tree, bind can be defined by the fold because every recursive child is a continuation branch. In a hefty tree, the fork contains computations which lie inside the operation’s scope. Sequencing after the operation must not be pushed into them.

The tempting equation is a full structural recursion: pure(x)bind×g=g(x),impure(o,ψ,k)bind×g=impure(o,λs.ψ(s)bind×g,λx.k(x)bind×g). For a general hefty node, the second line is not even well typed. If the whole node has result type A, then g has domain A, whereas ψ(s) returns RetForkH(o)(s), which need not be A. Equation (24.3) is typeable only in a homogeneous special case where every fork result in the node is A. The catch smart constructor (24.2.1) is such a case. There the equation typechecks and is still semantically wrong: it pushes g into both branches and also places g in the operation continuation: catch(M1,M2)bind×g=impure(catch(t),λb.(if b then M1 else M2)bind×g,g). Any elaborator which runs one branch and then invokes the continuation will therefore run g twice.

Definition 24.6 — Hefty bind

The hefty bind traverses only an operation’s continuation. For M:HeftyH(A),g:AHeftyH(B), define pure(x)=Hg=g(x),impure(o,ψ,k)=Hg=impure(o,ψ,λx.k(x)=Hg). Only the continuation is traversed. Put MHN:=M=H(λ_.N).

For catch, equation (24.6) gives catch(M1,M2)=Hg=impure(catch(t),λb.if b then M1 else M2,g). The branch selected as successful produces a value for g exactly once.

Example 24.7 — A visible double-sequencing error

Assume a lifted first-order operation tick:11. Let C=catch(pure(),pure()),g=λ_.tick. Under the correct bind, the selected pure branch returns to the continuation g, so one tick is produced. Under (24.3), the selected branch has already become tick, and the node continuation is also g; a catch elaborator which invokes the continuation after the selected branch therefore produces tickHtick. The defect is semantic, not a type error. Both trees have result type 1.

Theorem 24.8 — Monad equations for hefty bind

Let H be a higher-order signature and let A,B,C be sets. Fix x:A,M:HeftyH(A),f:AHeftyH(B),g:BHeftyH(C). Taking equality of operation nodes to compare their function fields extensionally, hefty bind satisfies pure(x)=Hf=f(x),M=Hpure=M,(M=Hf)=Hg=M=H(λx.f(x)=Hg).

Proof of Theorem 24.8 — Monad equations for hefty bind

Proof. The first equation is definitional. For the right unit, induct on M. The pure case is immediate. In the operation case, impure(o,ψ,k)=Hpure=(24.6)impure(o,ψ,λx.k(x)=Hpure)=IHimpure(o,ψ,k). The fork is unchanged, so no induction hypothesis is required there.

For associativity, the pure case is definitional. In the operation case, expand the left side twice: (impure(o,ψ,k)=Hf)=Hg=(24.6)impure(o,ψ,λx.(k(x)=Hf)=Hg)=IHimpure(o,ψ,λx.k(x)=H(λy.f(y)=Hg))=(24.6)impure(o,ψ,k)=H(λy.f(y)=Hg). Pointwise equality of the continuation fields gives constructor equality. ◻

Exercise 24.3 — The bad bind counted

★☆☆ Use the program in example 24.7, followed by a lifted operation readTicks:1N. Assume the target handler starts at zero, increments on tick, and returns the current count on readTicks. Calculate the result obtained with (24.6) and with (24.3).

Exercise 24.4 — Functorial action

★★☆ Define fmapH(f,M):=M=H(puref). Derive its two constructor equations and prove fmapH(id,M)=M,fmapH(gf,M)=fmapH(g,fmapH(f,M)).

The structural catamorphism

Elaboration has the opposite recursive requirement. It must translate every nested computation before the operation-specific clause is invoked. The algebra therefore receives already-translated forks and continuations.

Definition 24.9 — Hefty algebra

Let G:SetSet. A hefty algebra α:AlgH(H,G) consists, for every result set A, of an operation αA(o,,):(s:OpForkH(o)G(RetForkH(o)(s)))(RetHH(o)G(A))G(A) for each o:OpHH.

Definition 24.10 — Hefty catamorphism

Given a polymorphic family gX:XG(X) for every set X, and α:AlgH(H,G), the hefty catamorphism is the family catag,α,XH:HeftyH(X)G(X). In its defining equations, write CX:=catag,α,XH,Ro(s):=RetForkH(o)(s). Then CX(pure(x))=gX(x),CX(impure(o,ψ,k))=αX(o,λs.CRo(s)(ψ(s)),λx.CX(k(x))). We omit the final index when the domain determines it.

Equation (24.10) traverses both recursive fields. A bind with g:AHeftyH(B) cannot supply the polymorphic generator required at arbitrary fork-result types. Even in the homogeneous catch fragment, where those types coincide with A, structural recursion would transform the forks and recreate the semantic defect of (24.3).

Lemma 24.11 — Unique structural solution

Let (fX)X be a family fX:HeftyH(X)G(X) satisfying the two equations in (24.10) at every set X, for fixed g and α. Then, for every X, fX=catag,α,XH extensionally.

Proof of Lemma 24.11 — Unique structural solution

Proof. Apply the displayed induction principle of definition 24.3 to the simultaneously quantified predicate PX(M)fX(M)=catag,α,XH(M). The pure case is the common generator equation. In the operation case, the hypotheses give fYs(ψ(s))=cataYs(ψ(s)) for each fork and fX(k(x))=cataX(k(x)) for each response. Function extensionality equates both function fields, and the common algebra equation equates the results. ◻

Exercise 24.5 — Why bind is not this catamorphism

★★☆ Restrict to the catch-only homogeneous fragment at one fixed branch type A. Suppose there were a hefty algebra βg such that M=Hg=catag,βgH(M) for every A-result computation M, with the generator extended arbitrarily at unused result indices. Choose distinct values x,y:A and a constant function g:AHeftyH(B). Apply the operation equation to catch(pure(x),pure(x)) and catch(pure(y),pure(y)). Show that the catamorphism presents identical recursively folded fields to βg, whereas the correct binds in (24.3) retain different forks. Conclude that bind is not definable by the structural catamorphism interface.

Elaboration into first-order trees

The target of elaboration is an ordinary free tree. The source operation clause may arrange, handle, duplicate, or discard the translated computation arguments, but it cannot receive an ill-typed one.

Definition 24.12 — Elaboration

For a higher-order source signature H and an ordinary target signature Δ, an elaboration is a hefty algebra E:Elaboration(H,Δ):=AlgH(H,λA.FreeΔ(A)). Its induced translation is elaborateE:=catapure,EH:HeftyH(A)FreeΔ(A).

Unfolding the definition gives elaborateE(pure(x))=pure(x),elaborateE(impure(o,ψ,k))=EA(o,elaborateEψ,elaborateEk). These are the generic elaboration equations. An operation-specific clause is written against translated subcomputations, not raw source syntax.

Theorem 24.13 — Typing by construction

Assume E has the algebra type (24.12). For every set A, M:HeftyH(A)elaborateE(M):FreeΔ(A). More specifically, in an operation node the recursive calls supplied to the clause have exactly the types ψ^(s):FreeΔ(RetForkH(o)(s)),k^(x):FreeΔ(A).

Proof of Theorem 24.13 — Typing by construction

Proof. Induct on M. A pure node is translated by the target constructor pure:AFreeΔ(A). For an operation node, the induction hypotheses give (24.13) pointwise for the fork and continuation. The component EA(o,,) has exactly these two families as its premises and returns FreeΔ(A). This establishes (24.13). ◻

The theorem says neither that an untyped source parser produces a hefty tree nor that elaboration simulates a source operational semantics. It says that once source syntax is represented by the indexed family HeftyH, every well-typed algebra clause produces a target tree with the same result index.

Lifting ordinary operations

Let Nil be the empty ordinary signature. An ordinary signature Δ0 embeds into a higher-order signature Lift(Δ0) by OpLift(Δ0)H=OpΔ0,ForkLift(Δ0)(o)=Nil,RetLift(Δ0)H(o)=RetΔ0(o). A lifted operation has no nested computation arguments.

Definition 24.14 — Signature-row insertion

Use proof-relevant row insertion witnesses. For ordinary signatures, Ins(Δ;Δ0,Δ) is generated by XIns(Δ0sigΔ;Δ0,Δ)Ins(Δ;Δ0,Δ)Ins(Δ1sigΔ;Δ0,Δ1sigΔ). A witness w determines injections ιw:OpΔ0OpΔ,ιwr:OpΔOpΔ, together with the response-type transports induced by those injections. For higher-order signature rows, HIns(H;H0,H) is generated by the corresponding rules XHIns(H0H;H0,H)HIns(H;H0,H)HIns(H1H;H0,H1H). A witness wH determines the operation injection ιwHH:OpH0HOpHH, together with the induced fork and response transports used by the smart constructors above.

The witness of definition 24.14 extends a head injection. That injection appears in (24.2.1). The witness selects one occurrence of Catch and supplies the required fork and response transports.

For example, the throw summand in the transaction row has the concrete witness XIns(ThrowsigNil;Throw,Nil)Ins(Statesig(ThrowsigNil);Throw,StatesigNil). The outer rule skips State; the inner base rule selects Throw. The catch summand of the higher-order transaction row requires two skips. Put H2:=CatchLift(Nil),H1:=Lift(Throw)H2. Its exact witness is XHIns(H2;Catch,Lift(Nil))HIns(H1;Catch,Lift(Throw)Lift(Nil))HIns(Lift(State)H1;Catch,Lift(State)Lift(Throw)Lift(Nil)). The two outer rules skip the lifted state and throw summands; the base rule selects Catch. This is the witness suppressed later in (24.8).

Insertion witnesses are proof relevant. Consequently the implementation infers a witness from its indices only for a duplicate-free ordered row of atomic summands. On such rows, induction on the row proves that Ins and HIns have at most one derivation at fixed indices. If a row repeats a summand, the witness must remain explicit: different witnesses can select different occurrences, and this chapter claims no coherence theorem identifying their elaborations.

Proposition 24.15 — Canonical insertion on duplicate-free rows

Regard a right-associated signature row as an ordered list of atomic summands, and call it duplicate free when no summand identity occurs twice. If Δ is duplicate free and Δ0 is atomic, there is at most one pair consisting of a residual row Δ and a derivation of Ins(Δ;Δ0,Δ). If H is duplicate free and H0 is atomic, there is at most one pair consisting of a residual row H and a derivation of HIns(H;H0,H).

Proof of Proposition 24.15 — Canonical insertion on duplicate-free rows

Proof. Induct on the ordered row. The empty row has no derivation. If its head is the requested atomic summand, the base rule yields the tail as residual. A step derivation would require a second occurrence in the tail, which duplicate freeness excludes. If the head differs, the base rule is impossible and every derivation uses the step rule; the induction hypothesis uniquely determines both the tail residual and the tail derivation, hence also the residual with the skipped head restored. For HIns, induct on the ordered row H. The empty row has no derivation. If the head is H0, the base constructor determines the residual H as the tail; an HIns-Step derivation would require another H0 in that tail and contradict duplicate freeness. If the head differs from H0, HIns-Base cannot apply, so both candidate derivations end in HIns-Step. Their premises have the form HIns(Htail;H0,Htail); the induction hypothesis identifies both the tail residual and its derivation, after which the common skipped head identifies H and the two outer derivations. ◻

For w:Ins(Δ;Δ0,Δ), the ordinary-operation elaborator is Elift,w(o,ψ,k)=impure(ιw(o),kqw,o), where qw,o:RetΔ(ιw(o))RetΔ0(o) is the response transport. The fork ψ has empty domain by (24.5.1). Equation (24.5.1) simply reproduces the ordinary operation in the target row. The empty lifted signature has the unique algebra Enil, because there is no operation case to define.

Exercise 24.6 — Elaboration of a lifted operation

★☆☆ Let o:OpΔ0 and let o be the hefty smart constructor whose fork is empty and whose continuation is pure. Use (24.5) and (24.5.1) to calculate elaborateElift,w(o). State the response type of its target continuation, including the transport qw,o.

A modular catch elaborator

The target signature contains an ordinary exception operation throw:10.

Definition 24.16 — Throw handling and masking

Write Throw for this signature. If w:Ins(Δ;Throw,Δ), the ordinary throw handler has type runThroww:FreeΔ(A)FreeΔ(Maybe(A)). It maps a pure value to some(x), maps throw to none, and forwards every residual operation recursively. Its complete equations use the two-field target constructor impure(o,k):FreeΔ(A). The three-field impure(o,ψ,k) used earlier is the hefty constructor. Here res(o,k) abbreviates impure(ιwr(o),k), with the response transport induced by the injection. The equations are runThroww(pure(x))=pure(some(x)),runThroww(impure(ιw(throw),k0))=pure(none),runThroww(res(o,k))=impure(o,λr.runThroww(k(r))). Here k0:0FreeΔ(A) is the unique empty-domain continuation. The smart notation throw suppresses exactly this vacuous field. The same insertion witness gives effect masking maskw:FreeΔ(A)FreeΔ(A), which reinjects every residual operation into the larger row. Its equations are maskw(pure(x))=pure(x),maskw(impure(o,k))=res(o,λr.maskw(k(r))). In particular, masking does not add an operation node to a pure tree.

Definition 24.17 — Target free-tree bind

For target free trees, ordinary free-monad bind is the structural recursion pure(x)=f=f(x),impure(o,k)=f=impure(o,λr.k(r)=f). The three bind equations are pure(x)=f=f(x)Freeunit,t=pure=tFreeunitr,(t=f)=g=t=(λx.f(x)=g)Freeassoc. For target trees, write mn:=m=(λ_.n). This target abbreviation is distinct from the source sequencing MHN of definition 24.6. The first equation is the pure clause of (24.3). Induction on t proves the second and third equations; the impure case applies the induction hypothesis to every response branch.

We also use maybe(f,N)(some(x))=f(x),maybe(f,N)(none)=N.

Definition 24.18 — Catch elaboration

The tempting clause runThroww(ψ^(tt))=maybe(k^,ψ^(ff)=k^) is ill typed: the handled protected branch lies in FreeΔ, whereas both continuations return trees in FreeΔ. The missing map is precisely maskw:FreeΔFreeΔ.

For w:Ins(Δ;Throw,Δ), define Ecatch,w:Elaboration(Catch,Δ) by Ecatch,w,A(catch(t),ψ^,k^):=maskw(runThroww(ψ^(tt)))=maybe(k^,ψ^(ff)=k^).

The first branch is interpreted as protected computation. Its throw effect is handled locally, producing a possible result in the residual target row; mask then places that residual tree back in the full target row. On some(x), the operation continuation receives x. On none, the fallback runs and its result is supplied to the same continuation.

Proposition 24.19 — Typing of the catch clause

For the data supplied to the component in (24.18), every subexpression has the following type: ψ^(tt),ψ^(ff):FreeΔ(Val(t)),runThroww(ψ^(tt)):FreeΔ(Maybe(Val(t))),maskw(runThroww(ψ^(tt))):FreeΔ(Maybe(Val(t))),k^:Val(t)FreeΔ(A),ψ^(ff)=k^:FreeΔ(A),maybe(k^,ψ^(ff)=k^):Maybe(Val(t))FreeΔ(A). Consequently (24.18) has type FreeΔ(A).

Proof of Proposition 24.19 — Typing of the catch clause

Proof. The fork type is the first family of (24.9), specialized by (24.4). Equations (24.16) and (24.16) type the two nested transformations. The second family of (24.9) types the continuation, and ordinary free-monad bind types ψ^(ff)=k^. Both branches of (24.6) have type FreeΔ(A). Therefore maybe(k^,ψ^(ff)=k^):Maybe(Val(t))FreeΔ(A). Binding the masked computation to this function has type FreeΔ(A). ◻

Thus the catch algebra is well typed, which is the operation case of theorem 24.13. No separate preservation proof is needed because the branch and continuation result indices are arguments of the clause type itself.

Example 24.20 — Complete elaboration of successful catch

Let x:Val(t) and M:HeftyH(Val(t)). Combine the catch elaborator with whatever components translate the operations of M, and write the combined elaborator as E. Put N=elaborateE(M). Then elaborateE(catch(pure(x),M))=(24.5),(24.18)maskw(runThroww(pure(x)))=maybe(pure,N=pure)=(24.1),(24.2)pure(some(x))=maybe(pure,N=pure)=Freeunitpure(x). The fallback is translated structurally before the catch clause is invoked, but it is not placed in the resulting target tree when the protected branch is already pure and successful.

Example 24.21 — Complete elaboration of thrown catch

Let throwH be the lifted source throw operation. Its lift elaborator produces the target throw node. Put N=elaborateE(M). Therefore elaborateE(catch(throwH,M))=(24.5),(24.18)maskw(pure(none))=maybe(pure,N=pure)=(24.2)pure(none)=maybe(pure,N=pure)=FreeunitN=pure=FreeunitrN. The throw is handled during elaboration of the catch node; it is not exposed to a later top-level throw handler.

Exercise 24.7 — Continuation after fallback

★☆☆ Let f have type Val(t)HeftyH(B). Calculate the elaboration of catch(throwH,M)=Hf through the point at which the target translation of M is sequenced with that of f. Identify the single occurrence of the translated continuation.

Composition by disjoint cases

A global elaborator should contain no clause which knows how many other operations exist. Disjoint signature sum makes that requirement a case split.

Definition 24.22 — Composition of hefty algebras

For E1:AlgH(H1,G),E2:AlgH(H2,G), define E1E2:AlgH(H1H2,G) by the two equations (E1E2)A(inl(o),ψ,k)=E1,A(o,ψ,k),(E1E2)A(inr(o),ψ,k)=E2,A(o,ψ,k). The operation composes elaboration clauses; it is unrelated to logical disjunction. Its unparenthesized chains associate to the right.

Theorem 24.23 — Modular elaboration equations

Let Ei:Elaboration(Hi,Δ)(i=1,2). Then E1E2 is an elaboration of H1H2 into the same target signature Δ, and elaborateE1E2(impure(inl(o),ψ,k))=E1,A(o,elaborateE1E2ψ,elaborateE1E2k),elaborateE1E2(impure(inr(o),ψ,k))=E2,A(o,elaborateE1E2ψ,elaborateE1E2k). Thus adding E2 does not alter the clause defining E1, although both clauses recursively use the same combined elaborator on their children.

Proof of Theorem 24.23 — Modular elaboration equations

Proof. The algebra type follows from (24.2): every operation is either an H1-operation or an H2-operation, and its fork and return types reduce to those of the selected summand. Expand (24.5), then apply the corresponding equation of (24.22). No other case occurs. ◻

For three higher-order signatures, define the reassociation bijection on operation tags by ρ(inl(inlo))=inlo,ρ(inl(inro))=inr(inlo),ρ(inro)=inr(inro). Its inverse uses the three equations in reverse. Case reduction in section 24.2 gives, for every tag u, canonical bijections OpFork(H1H2)H3(u)OpForkH1(H2H3)(ρu) and equal response sets at corresponding fork tags, together with Ret(H1H2)H3H(u)RetH1(H2H3)H(ρu). These maps are the identity after the relevant Hi-case is selected. The reassociation transport sends an operation by ρ, reindexes each fork along the first bijection and its response equality, and reindexes the continuation along the last bijection. These are the three transports used below.

Proposition 24.24 — Composition coherence under reassociation

Composition is associative after transport along the canonical reassociation bijection between (H1H2)H3 and H1(H2H3). The two source types are not definitionally identical; after transporting operation, fork, and response indices, both composite algebras agree extensionally.

Proof of Proposition 24.24 — Composition coherence under reassociation

Proof. Case-analyze the transported operation tag. An H1-tag dispatches to E1 on both sides, an H2-tag to E2, and an H3-tag to E3. In every case the fork and continuation are recursively translated by the same combined elaborator. Pointwise equality at every fork and response index, followed by function extensionality, equates the two transported function fields. ◻

Exercise 24.8 — Associativity under reassociation

★★☆ Define the canonical operation-tag bijection ((OpH1H+OpH2H)+OpH3H)(OpH1H+(OpH2H+OpH3H)). Check all three operation-tag cases and show that (E1E2)E3 and E1(E2E3) agree after transporting along this bijection. Explain why literal equality without transport has the wrong type.

State, throw, and catch in one tree

Let State be the ordinary signature with operations put:N1,get:1N. Thus put(n) is the operation instance carrying parameter n:N; its response is :1. Consider the source and target signatures Htr:=Lift(State)Lift(Throw)CatchLift(Nil),Δtr:=StatesigThrowsigNil. The component elaborators compose as Etr:=Elift,StateElift,ThrowEcatchEnil. The displayed rows are duplicate free, so proposition 24.15 gives one insertion witness at each component boundary. Equation (24.8) records the semantic order of the four components.

The transaction is transact:=put(1)Hcatch(put(2)HthrowH,pure())Hget. The catch handles the throw, but its target clause forwards state. Hence the state update to 2 survives the failed protected branch.

Definition 24.25 — State handling

The state handler used here deliberately discards the final store. For a residual signature ε, define runStates:FreeStatesigε(A)Freeε(A) by runStates(pure(x))=pure(x),runStates(put(n,k))=runStaten(k()),runStates(get(k))=runStates(k(s)),runStates(res(o,k))=impure(o,λr.runStates(k(r))).

This is the finite model’s hSt observation. The handler of chapter 22 that returns (x,s) is a different interface; substituting it would change the proposition’s result type.

Proposition 24.26 — Global-state transaction calculation

Let runState0 be the ordinary deep state handler started at 0, and let un eliminate the empty target signature. Then un(runThrow(runState0(elaborateEtr(transact))))=some(2).

Proof of Proposition 24.26 — Global-state transaction calculation

Proof. First expose the catch clause. The protected target tree is put(2)throw. The local throw handler forwards the put and handles the following throw: runThrow(put(2)throw)=(24.1) (forward)put(2)runThrow(throw)=(24.1) (throw)put(2)pure(none). Masking reinjects this residual state tree into Δtr. Expose the none branch, fallback bind, and unit conversions separately. Abbreviate the forwarded target and the fallback by t0:=put(2)pure(none),t1:Ty,Val(t1)=1,k^:=λ_.get,f:=pure()=k^,q:=maybe(k^,f),ctr:=Ecatch(catch(t1),λb.if b then(put(2)throw) else pure(),k^). Then the remaining calculation fits on one semantic step per line: elaborateEtr(transact)=liftput(1)andheftybindput(1)ctr=(24.18)put(1)(mask(runThrow(put(2)throw))=q)=(24.8)put(1)(mask(put(2)pure(none))=q)=(24.2),abbreviationsput(1)(t0=q)=Freeassocput(1)put(2)(pure(none)=q)=(24.6),nonebranchput(1)put(2)f=Freeunitput(1)put(2)get. Now use the four equations of equation 24.4; no state transition is hidden in prose: runState0(put(1)put(2)get)=(24.4) (put)runState1(put(2)get)=(24.4) (put)runState2(get)=(24.4) (get)runState2(pure(2))=(24.4) (pure)pure(2). Consequently runState0(elaborateEtr(transact))=pure(2). Finally, the pure equation of equation 24.1 gives runThrow(pure(2))=pure(some(2)),un(pure(some(2)))=some(2), which is the displayed result. ◻

The source operation is an interface, not a fixed semantics. Replace only the catch component by Eprotected(catch(t),ψ,k):=ψ(tt)=k. This component always runs the protected branch and never handles its throw. All other components in (24.8) remain unchanged. The same source transaction then reaches the outer throw handler and returns none. The change is local to one algebra clause.

Exercise 24.9 — Alternative catch component

★★☆ Replace Ecatch in (24.8) by (24.8). Calculate the elaborated target tree through the state handler and then the throw handler. Show that the final result is none, while the state update to 2 is performed before the throw aborts the remaining continuation.

Lawfulness is stated after observation

Intrinsic typing ensures that the catch clause returns a target tree of the right result type. It does not ensure that the clause behaves like exception catch. Lawfulness is an equational obligation on the interpretation obtained after elaboration and handling.

For this section put the target throw signature at the front: Throwsigε. Let hA:FreeThrowsigε(A)Freeε(Maybe(A)) be the throw handler, and let maskA:Freeε(A)FreeThrowsigε(A) be masking. These are the front-summand instances of runThroww and maskw from (24.1), (24.2); this section suppresses the unique canonical witness and writes h and mask to keep the law equations legible. Let E0:Elaboration(H,Throwsigε) be any elaboration for the remaining higher-order operations, and define E:=Elift,ThrowEcatchE0,run(M):=h(elaborateE(M)). Thus run:HeftyLift(Throw)CatchH(A)Freeε(Maybe(A)).

Two structural facts about the target handler contain the proof mechanism.

Lemma 24.27 — Throw handling distributes through bind

For m:FreeThrowsigε(A),f:AFreeThrowsigε(B), we have h(m=f)=h(m)=maybe(hf,pure(none)).

Proof of Lemma 24.27 — Throw handling distributes through bind

Proof. Induct on m. If m=pure(x), both sides are h(f(x)). If m is throw, both sides are pure(none). If m is a residual operation impure(o,k), handling and bind both forward it. The two sides are operation nodes with continuation families rh(k(r)=f)andrh(k(r))=maybe(hf,pure(none)), which agree pointwise by the induction hypothesis. ◻

Lemma 24.28 — Handling a masked tree

For n:Freeε(A), h(maskn)=n=(puresome).

Proof of Lemma 24.28 — Handling a masked tree

Proof. Induct on n. A pure value is mapped to pure(some(x)) on both sides. A residual operation is forwarded by mask, then forwarded by h; ordinary bind also forwards it. The continuation families agree by the induction hypothesis. ◻

These lemmas reduce the higher-order catch operation to an ordinary equation on handled results.

Proposition 24.29 — Observable catch equation

Fix t:Ty, put A=Val(t), and take M1,M2:HeftyLift(Throw)CatchH(A). Put Ri=run(Mi). Then run(catch(M1,M2))=R1=maybe(puresome,R2).

Proof of Proposition 24.29 — Observable catch equation

Proof. Write mi=elaborateE(Mi), and abbreviate q2=maybe(pure,m2),Q2=maybe(hq2,pure(none)). Expand the smart constructor, then the catch algebra, and only then use the right unit: run(catch(M1,M2))=(24.2.1)h(Ecatch(catch(t),λb.mb,pure))=(24.18)h(mask(h(m1))=maybe(pure,m2=pure))=Freeunitrh(mask(h(m1))=maybe(pure,m2)). Apply lemma 24.27 and then lemma 24.28: h(mask(h(m1))):Freeε(Maybe(Maybe(A))),z:Maybe(A). The outer Maybe records the result of handling the masked tree; the inner one is the result already produced by h(m1). (24.9)=lemma24.27h(mask(h(m1)))=Q2=lemma24.28(h(m1)=(puresome))=Q2=Freeassoch(m1)=(λz:Maybe(A).(pure(some(z)))=Q2)=Freeunith(m1)=(λz.Q2(some(z)))=(24.6)h(m1)=(λz.h(q2(z))). For z=some(x), the final function returns pure(some(x)); for z=none, it returns h(m2). Substituting Ri=h(mi) gives (24.29). ◻

Theorem 24.30 — Lawfulness equations for modular catch

Fix codes tA,tB:Ty. Put A=Val(tA) and B=Val(tB). Fix a residual higher-order signature H, and put Hc=Lift(Throw)CatchH. Let x:A,k:AHeftyHc(B),M,M1,M1,M2,M2:HeftyHc(A). Write throwXH:HeftyHc(X) for the lifted empty-response throw at result index X. The interpretation (24.9) satisfies run(throwAH=Hk)=run(throwBH),(bind--throw)run(catch(pure(x),M))=run(pure(x)),(catch--return)run(catch(throwAH,M))=run(M),(catch--throw1)run(catch(M,throwAH))=run(M),(catch--throw2). Moreover, if run(M1)=run(M1),run(M2)=run(M2), then run(catch(M1,M2))=run(catch(M1,M2)).

Proof of Theorem 24.30 — Lawfulness equations for modular catch

Proof. A lifted throw has no response. Hefty bind changes only its impossible continuation, so bind–throw is immediate after elaboration and handling.

For the remaining equations use (24.29). Since run(pure(x))=pure(some(x)),run(throwH)=pure(none), catch–return and catch–throw1 are the two left-unit cases of (24.29). For catch–throw2, the branch function becomes maybe(puresome,pure(none))=pure, pointwise on Maybe(A). Equation (24.29) therefore reduces to the right unit of target bind.

For (24.30), substitute the first assumed equality for the left operand of the bind in (24.29). The second assumed equality says that the two branch functions agree at every returned value. Function extensionality equates those functions, and congruence of target bind gives the result. ◻

The equalities are deliberately stated after run. Two source trees may elaborate to different target trees yet become equal after the throw handler observes them. Replacing (24.30) by raw syntactic equality would state a stronger and generally false interface.

Exercise 24.10 — A derived nested-catch law

★★☆ Using only proposition 24.29 and the free-monad laws, prove run(catch(catch(M1,M2),M3))=run(catch(M1,catch(M2,M3))). Do not claim equality of the two source trees.

Exercise 24.11 — Typing versus lawfulness

★☆☆ For each of the following claims, state whether it follows from theorem 24.13, from theorem 24.30, or from neither:

  1. the target tree has the same result index as the source hefty tree;

  2. catch satisfies catch–throw after observation;

  3. elaboration simulates every source reduction step;

  4. every target operation is handled after finitely many target steps;

  5. a parser and typechecker construct the intended indexed source tree.

Give one sentence identifying the missing definition or theorem for every claim answered “neither.”

Three syntaxes, three invariants

Scoped syntax and hefty trees both add recursive positions beyond an ordinary continuation, but enforce different invariants:

Syntax Node data beyond an operation Recursive invariant Modular interpretation step
Ordinary free tree Response continuation k(r) Fold recurses through every response branch One first-order algebra component
Scoped syntax of chapter 23 Ordinary arguments, a distinguished scoped computation, continuation, and explicit substitution structure Substitution distributes through each declared scoped position and preserves binding An elementwise interpretation respecting the scoped-substitution equations
Hefty tree Fork family ψ(s) plus an ordinary continuation k(r) Catamorphism recurses through both fields; monadic bind recurses only through k One hefty algebra component receiving already elaborated forks and continuation

A scoped signature distinguishes one particular binding pattern and carries explicit substitution equations. A hefty signature records a dependent family of computation arguments and leaves their target arrangement to the elaboration clause. A translation must map the scoped body and continuation to explicit fork and response indices, and it must commute with scoped substitution: translating after substitution must equal target bind after translation. The constructor shapes alone establish none of these equations.

Mechanized boundary.

The published Agda development formalizes the indexed definitions, modular composition, catch equations, and concrete transaction normalizations used here. Its lawfulness interface assumes function extensionality, and its bind-throw field proves the homogeneous result-code instance; the heterogeneous form in theorem 24.30 is proved locally. The finite Kappa capsule checks the displayed elaboration equations. Neither artifact contains a source operational semantics or a source–target simulation theorem; appendix E records the exact archive and executable boundaries.

The definitions and selected examples in this chapter follow Poulsen and van der Rest’s intrinsically typed Agda development and its accompanying paper [PvdR23]. The proofs are reconstructed locally at the signatures displayed here; the citation supplies provenance, not omitted premises.

Suggested first pass.

Begin with exercise 24.12, exercise 24.13, continue with exercise 24.14, and finish with the practical project exercise 24.17.

Exercise 24.12

★★★ For the three-branch signature of exercise 24.2, take ForkFirst(first(t))=({0,1,2},λ_.Val(t)),RetFirstH(first(t))=Val(t). Thus this exercise is independent of the earlier exercise’s answer. Define an elaborator which runs the branches from left to right, treating target throw as failure and returning the first successful value. If all three branches throw, emit one target throw. Give the complete algebra clause, including every mask, handler, bind, and maybe; then derive the types of all intermediate terms. Calculate its action on first(throwH,pure(2),pure(3)).

Exercise 24.13

★★☆ Starting from (24.18), reprove catch–throw2 directly by structural induction on the elaborated target tree, without using proposition 24.29. State the induction predicate, and give the pure, throw, and forwarded-operation cases. Compare the resulting proof with the factorized proof through lemma 24.27, lemma 24.28.

Exercise 24.14 — (*)

★★☆ Define the pair-returning observation runStates×. Replace only the pure equation of equation 24.4 with runStates×(pure(x))=pure(x,s) and retaining the put, get, and forwarding equations. Put T:=elaborateEtr(transact). Calculate runThrow(runState0(T))andrunThrow(runState0×(T)). Explain why the results are some(2) and some(2,2), respectively, and why this comparison changes only the observation of final state, not catch’s rollback policy.

Exercise 24.15 — (*)

★★★ Generalize exercise 24.8 to a finite binary tree of higher-order signature sums. Define a canonical flattening to a list of summands, transport every operation, fork, and return index along the flattening, and prove that any two parenthesizations of the same ordered list of component elaborators agree after transport. Explain why permutation of the list is an additional theorem rather than part of reassociation.

Exercise 24.16

★★★ Consider the finite source grammar M::=return(n)put(n)get(x.M)throwcatch(M,M)M;M and configurations s,M. Read this as a sorted grammar: put(n):1, get:N, catch’s branches have one result sort, and sequencing discards its left result. Define the embedding into the intrinsically sorted hefty syntax by return(n)=pure(n),put(n)=putH(n),get(x.M)=getH=H(λx.M),throw=throwH,catch(M,N)=catch(M,N),M;N=MHN. The transaction from (24.8) is now expressible using the final sequencing clause. The root steps are s,return(n);MHs,M,s,put(n);MHn,M,s,get(x.M)Hs,M[s/x],s,catch(return(n),N)Hs,return(n),s,catch(throw,N)Hs,N, plus left congruence under catch and sequencing. Relate s,M to the target tree runStates(elaborateEtr(M)). For each of the five roots, show a nonempty target calculation to the tree related to the source reduct; in the catch–throw case expose the catch clause before using equation 24.1, equation 24.2. Then state one property still outside this finite simulation (for example, inference into intrinsically typed hefty syntax or absence of unhandled operations for arbitrary sums).

Exercise 24.17

★★★ Practical project.hefty-elaboration Implement the finite source and target fragments used in example 24.7, proposition 24.26. The source must represent pure nodes, lifted state and throw operations, a catch node with two computation arguments, the correct bind (24.6), the incorrect structural bind (24.3), the catamorphic elaborator (24.5), and composition by operation-tag dispatch. The target must implement free-tree bind, state handling, throw handling, and empty-signature elimination.

Maintain this invariant: every node carries a result-type tag; both catch branches have the operation’s declared branch type; the operation continuation returns the node’s result type; and each source operation tag is accepted by exactly one elaboration component. Produce four decidable checks:

  1. the global transaction returns some(2);

  2. replacing only the catch component by (24.8) returns none;

  3. the correct bind makes the counted example return 1, while (24.3) makes it return 2;

  4. constructing a catch whose branches have different result tags is rejected before elaboration.

The accepted run must report all four outcomes and terminate unsuccessfully if any expected value changes.

Search the book

Type to search the local edition.