Lectures onType Theory
ch:algebraic-effects: ch:algebraic-effects
appendix sectionsolutions

ch:algebraic-effects: ch:algebraic-effects

exercise 22.1.

Let the initial counter be n. Call by value evaluates the argument before beta reduction: (λx.())tick()(λx.())()(),nn+1. Call by name substitutes the unevaluated argument: (λx.())tick()(),nn. The traces first differ at argument evaluation: the value strategy performs the tick before beta reduction, while the name strategy takes beta first and the absent occurrence of x discards the argument.

exercise 22.2.

A raise request never receives a response, so its response-indexed family is the unique map absurd0:0TΣA. Giving it response type 1 would supply a branch k(()):TΣA. That branch falsely depicts an execution in which the outside world acknowledges the exception and the computation after the raise continues.

exercise 22.3.

Let k:RopTΣA, f:ATΣB, and g:BTΣC. Then (Opop(p,k)=f)=g=Opop(p,λr:Rop.(k(r)=f)=g)=Opop(p,λr:Rop.k(r)=(λa.f(a)=g))=Opop(p,k)=(λa.f(a)=g). The middle equality uses the induction hypothesis at every r:Rop. By the branch equality stipulated in definition 22.1, those pointwise equalities are exactly equality of the two continuation branches; no additional function-extensionality axiom is used.

exercise 22.4.

With result S, take t1=Opget((),λs.Opget((),λ_.Ret(s))) and t2=Opget((),λs.Ret(s)). A usual state theory identifies them: the second read, with no intervening write, returns the same store as the first. They are unequal raw trees. After constructor injectivity removes their common outer get node, the branch of t1 begins with another operation node whereas the corresponding branch of t2 is a return node; distinct constructors cannot be equal.

exercise 22.5.

Use carrier List(A) and r(a)=[a],hchoose((),k)=k(false)++k(true), where ++ is list concatenation. The fold equation is therefore fold(Opchoose((),k))=fold(k(false))++fold(k(true)), so every false subtree is explored before its corresponding true subtree. Well-foundedness makes each result list finite.

exercise 22.6.

If a closed terminal M has type FA, inversion of terminal canonical forms gives M=returnV. If it has type AC, the same lemma gives M=λx.N. Hence one fixed well-typed terminal cannot have both shapes. The decisive facts are inversion of C-Return and C-Lam: the former concludes only an F type and the latter only a computation arrow.

exercise 22.7.

For c:b, the translation is return(thunk(λf.(returnf to g.returnc to a.(forceg)a)))to q.return(thunk(λx.returnx)) to i.(forceq)i. The binders have types q:U(U(bFb)Fb),i,f,g:U(bFb),a:b. Two To steps, Force, and Beta expose the body. Its two To steps bind g=i and a=c; two more Force/Beta steps reduce the identity call to returnc.

exercise 22.8.

Rename binders fresh. For abstraction, ((λy.e)[u/x])n=λy.(e[u/x])naλy.en[thunkun/x]=(λy.e)n[thunkun/x]. For application, congruence and the two induction hypotheses give ((e1e2)[u/x])n=(e1[u/x])n(thunk(e2[u/x])n)ae1n[thunkun/x](thunk(e2n[thunkun/x]))=(e1e2)n[thunkun/x]. Weak reduction does not enter the lambda in the first calculation. It therefore cannot contract a force–thunk redex created in its body; a, whose compatible closure includes lambda bodies, is necessary.

exercise 22.9.

The outer application translates to (λx.hn(thunk(forcex))(thunk(forcex)))(thunkun), where each displayed source application uses the call-by-name application clause and the curried type of h determines the parentheses. Beta reduction yields hn(thunk(force(thunkun)))(thunk(force(thunkun))). Thus the substituted thunk for u occurs twice, once below each thunk passed to h; the two occurrences of xn=forcex are the two forces. Each is run only if the corresponding argument of h is demanded.

exercise 22.10.

In context x:1, the operation rule gives raisee0(z.returnz):F0!{raise}. Hence C-LamΣ gives λx.raisee0(z.returnz):1{raise}F0!, and V-ThunkΣ gives the value type U(1{raise}F0). The thunk has no immediate computation effect because it is a value; the lambda computation also has empty immediate effect; entering the arrow body has latent effect {raise}.

exercise 22.11.

Let Σ(choose)=1Bool and let the handler preserve result type A. Put Ein={choose}D and choose DEout. Its return premise is Γ,x:Acreturnx:FA!Eout, using C-Weaken. In the choose clause, p:1,k:U(BoolEoutFA). Both (forcek)false and (forcek)true have FA!Eout by C-ForceΣ and C-AppΣ; their sequencing has the same annotation. The two occurrences on the continuation arrow are exactly the output effect that types the two resumptions. Finally Ein{choose}Eout is the forwarding premise.

exercise 22.12.

Under the proposed side condition, every input effect must also be an output effect. Hence an exception handler with Ein={raise} can receive only an output annotation satisfying {raise}Eout. The smallest inferred-looking result is therefore handleMwithHE:FA!{raise}, even though HE has a well-typed raise clause and no raise can escape. The rule remains safe because it forgets no possible operation; progress may still expose only names retained in the output. It fails as an elimination rule because handling raise can never remove that name.

exercise 22.13.

For example, instantiate the callback effect separately at , {raise}, and {get,put}: U(AFA)(AFA),U(A{raise}FA)(A{raise}FA),U(A{get,put}FA)(A{get,put}FA). The same callback effect appears on the input arrow and the returned arrow; the surrounding thunk and outer arrow remain pure. A quantified effect variable must abstract exactly those two repeated finite sets.

exercise 22.14.

If the program reads s, writes s+1, and returns a, no raise occurs. Both orders give Catch(State(t)(s0))=Ret(a,s0+1)=State(Catch(t))(s0). If the raise is moved before the write, that write lies in the impossible continuation. State first forwards the raise before changing the store and the outside catch returns (a0,s0); catch first replaces the raise by Ret(a0) before state sees any write. Thus both orders give Ret(a0,s0). Neither modified program distinguishes the two orders. Only the original write-then-raise program distinguishes rollback Ret(a0,s0) from commit Ret(a0,s0+1).

exercise 22.15.

The outer choose is handled and the clause first resumes at b1=false. With the erroneous shallow continuation, that resumption is choose()(b2.returnb2) without an enclosing Htwice. This inner choose is the first exposed request. The deleted occurrence handleX[M[y/x]]withHtwice inside k^ is precisely the handler which would capture it. Reinstalling only around the clause, rather than inside each resumption, is too late.

exercise 22.16.

Let HE have exception and return clauses but no get clause. One forwarding step is handle(get()(s.returns))withHEget()(y.handle(returny)withHE). The rebuilt continuation is y.handle(returny)withHE; it retains the exception handler. Put this term under a state handler HS. The get node is now captured by that nearest HS, which supplies the current store to y; the preserved continuation then returns that value through HE.

exercise 22.17.

Suppose ΓcX[choose()(x.M)]:FA!Ein and the handler produces FB!Eout. Decomposition gives Γ,x:BoolcM:FA!E0. For fresh y, response substitution gives Γ,y:BoolcM[y/x]:FA!E0. Effect weakening gives the same term annotation {choose}E0, the annotation of the hole. Replacement therefore types X[M[y/x]] at the handler input. Applying C-Handle, then C-LamΣ and V-ThunkΣ, gives Γvk^:U(BoolEoutFB). Parameter substitution replaces p:1 by () in the choose-clause premise; continuation substitution then replaces k by k^. The resulting clause body has exactly FB!Eout, the type of the reduct.

exercise 22.18.

For Htwice the return component is r(a)=Ret(a) and its choose component is hchoose((),g)=g(false)=(λ_.g(true)). The input reifies as Opchoose((),λb.Ret(b)). The handled-operation step reifies on both sides as Ret(false)=(λ_.Ret(true))=Ret(true). At each resumption, force(thunk(λb.P))V and then (λb.P)V both reify as P[V/b] by the two administrative clauses. Thus the operation step and both steps at each resume satisfy the theorem’s one-step equality, and the final operational result reifies as the fold.

exercise 22.19.

For E=2×[] and body k3, the original term captures the doubling context: reset(2×shift k.k3)reset((λx.reset(2x))3)6. After pushing the context into the body, the captured context is empty: reset(shift k.2×(k3))reset(2×((λx.reset(x))3))6. So this one-invocation instance happens to be equal. With body k(k3), the original term invokes the captured doubling context twice and gives reset((λx.reset(2x))((λx.reset(2x))3))12. The pushed term captures only the empty context and gives reset(2×((λx.reset(x))((λx.reset(x))3)))6. Thus the would-be commutation law fails when the captured context is invoked twice.

exercise 22.20.

For the one-handler term, C-Op gives its body input Ein={op}, and the pure clauses let C-Handle choose Eout=. In the two-handler term, the inner instance has the same two annotations. Its handled result is therefore typed with input at the outer instance, whose output is again .

If scoped availability itself is represented by a finite set, one layer is {op} and two layers are {op}{op}. Finite-set idempotence gives {op}{op}={op}. Consequently the annotation cannot answer whether eliminating the nearest handler occurrence should leave another scoped occurrence of op behind. Set subtraction removes the only recorded name in both cases, so it cannot express one-occurrence removal.

Search the book

Type to search the local edition.