Delimited continuations
We’ve looked at several kinds of effects – state, exceptions, generators, and nondeterminism – and seen that we can translate them into pure lambda calculus, but the translation is a whole-program transformation, not a macro translation. Here, we introduce a single extension to the lambda calculus that does make it possible to macro-express these other effects.
Motivation
Take a look at some of the evaluation rules for exceptions, generators, and nondeterminism:
try F[error] with t2 ⟶ t2 E-TryConErr
gen F[yield v] ⟶ inr {v, λz. gen F[unit]} E-GenConYield
collect F[amb v1 v2] ⟶ collect F[v1] ++ collect F[v2] E-CollectConAmb
All of them have a “delimiter” (try, gen,
collect) and one or more kinds of expressions
(error, yield, amb, etc.) that
can be used inside the delimiter. All of them use a special kind of
context (F[·]) that contains the rest of the computation
within the delimiter. Ordinarily, reductions inside the delimiter look
like
collect F[pred (succ v)] ⟶ collect F[v] E-PredSucc, E-Context
But the rules E-TryConErr, etc., all do something different: they use
F[·] zero or two times and/or wrap F[·] inside
something else.
Here, we want to invent a new control operator that makes
F[·] into a function, called a
continuation, so that the program can do whatever it wants with
the continuation. It can call it zero or two times, or it can wrap it
inside something else. Thus it will be able to macro-express exceptions,
generators, and nondeterminism.
Syntax and semantics
We introduce two new keywords, shift0 and
reset.
t ::= ...
reset t
shift0 t
E ::= ...
reset E
shift0 E
F ::= ...
shift0 F
reset v ⟶ v
reset F[shift0 v] ⟶ v (λx. reset F[x])
shift0 expects its argument to be a function, and it’s
normally called like shift0 (λk. t). Let’s call
t the body. The shift0 expression takes the
rest of the computation inside the innermost reset and
packages it into a continuation. Then the body is evaluated with
k bound to the continuation.
A trivial use of shift0 is
shift0 (λk. k true):
reset F[shift0 (λk. k true)] ⟶ (λk. k true) (λx. reset F[x])
⟶ (λx. reset F[x]) true
⟶ reset F[true]
So shift0 (λk. k true) is equivalent to
true.
Simulating other effects
Exceptions
What does shift0 (λk. true) do?
reset F[shift0 (λk. true)] ⟶ (λk. true) (λx. reset F[x])
⟶ true
This is similar to an exception. More generally, exceptions can be implemented like this:
try t1 with t2 ≡ case reset (inr t1) of
inl x ⇒ t2 x
| inr x ⇒ x
raise ≡ λe. shift0 (λk. inl e)
Recall that in the semantics of exceptions,
try v1 with t2 ⟶ v1. Let’s see if our simulated
exceptions do the same.
try v1 with t2
≡ case reset (inr v1) of inl x ⇒ t2 x | inr x ⇒ x
⟶ case inr v1 of inl x ⇒ t2 x | inr x ⇒ x
⟶ v1
In the semantics of exceptions, the reduction for handling an
exception is try F[raise v11] with t2 ⟶ t2 v11.
try F[raise v11] with t2
≡ case reset (inr F[(λe. shift0 (λk. inl e)) v11]) of inl x ⇒ t2 x | inr x ⇒ x
⟶ case reset (inr F[shift0 (λk. inl v11)] of inl x ⇒ t2 x | inr x ⇒ x
⟶ case (λk. inl v11) (λx. reset (inr F[x])) of inl x ⇒ t2 x | inr x ⇒ x
⟶ case inl v11 of inl x ⇒ t2 x | inr x ⇒ x
⟶ t2 v11
Above, the body of the shift0 did not call
k. If it did, then evaluation of the reset
expression would resume right where it left off. This could be used to
simulate resumable exceptions.
Generators
Instead of having the body of shift0 call k
right away, it could return k. Then we could save
k until much later and still be able to call k
to resume evaluating the reset expression. This is similar
to generators.
Generators can be simulated like this:
gen t ≡ reset (inl t)
yield ≡ λo. shift0 (λk. inr {o, k})
If t never yields, then the generator evaluates to
something of the form inl v.
When t yields o, control exits the
generator with value inr {o, k}. At any point, we can call
k i to resume the generator, as if i were the
value of the yield expression.
Nondeterminism
What does shift0 (λk. {k false, k true}) do?
reset F[shift0 (λk. {k false, k true})]
⟶ (λk. {k false, k true}) (λx. reset F[x])
⟶ {(λx. reset F[x]) false, (λx. reset F[x]) true}
⟶ {reset F[false], reset F[true]}
Calling the continuation twice evaluates the context
F[·] twice with two different values for the hole. This is
similar to nondeterminism.
More generally, you can call k multiple times to resume
evaluation multiple times. Thus, nondeterminism can be implemented like
this:
collect t ≡ reset [t]
amb ≡ λx1. λx2. shift0 (λk. (k x1) ++ (k x2))
Calling amb x replaces the rest of the
reset with a term that says, resume the reset
once for each element of x. Each of those evaluates to a
list, and those lists are concatenated together to produce a final
answer.
collect F[v] ≡ reset [v]
= [v]
collect F[amb v1 v2] ≡ reset (cons F[(λx. shift0 (λk. (k v1) ++ (k v2)))] nil)
⟶ reset (cons F[shift0 (k v1) ++ (k v2))] nil)
⟶ (λk. (k v1) ++ (k v2)) (λx. reset (cons F[x] nil))
⟶ ((λx. reset (cons F[x] nil)) v1) ++ ((λx. reset (cons F[x] nil)) v2)
⟶ (reset (cons F[v1] nil)) ++ (reset (cons F[v2] nil))
≡ collect F[v1] ++ collect F[v2]
State
In the previous example, we saw that if you put something (e.g.,
++) around a call to k, that thing ends up
around the reset expression.
This is true no matter what the “something” is. For example, you can
use shift0 to make a λs appear outside of the
reset:
reset F[shift0 (λk. λs. k s)] 0
⟶ (λk. λs. k s) (λx. reset F[x]) 0
⟶ (λs. (λx. reset F[x]) s) 0
⟶ (λx. reset F[x]) 0
⟶ reset F[0]
So shift0 (λk. λs. k s) grabs the first term after the
reset expression.
Similarly, you can make an extra argument appear to the right of the
reset:
reset F[shift0 (λk. k unit 0)]
⟶ (λk. k unit 0) (λx. reset F[x])
⟶ (λx. reset F[x]) unit 0
⟶ reset F[unit] 0
So shift0 (λk. k unit s) makes s appear
after the reset expression and evaluates to
unit.
Putting these two tricks together, we can use the place to the right
of reset as a mutable state.
init s in t ≡ reset (let x' = t in λs'. x') s
get ≡ shift0 (λk. λs. k s s)
set ≡ λs'. shift0 (λk. λs. k unit s')
init 0 in (set (succ get); get)
≡ reset (let x' = (set (succ shift0 (λk. λs. k s s)); get) in λs'. x') 0
⟶ (λk. λs. k s s) (λx. reset (let x' = (set (succ x); get) in λs'. x')) 0
⟶ (λs. (λx. reset (let x' = (set (succ x); get) in λs'. x')) s s) 0
⟶ (λx. reset (let x' = (set (succ x); get) in λs'. x')) 0 0
⟶ reset (let x' = (set (succ 0); get) in λs'. x') 0
≡ reset (let x' = ((λs'. shift0 (λk. λs. k unit s')) (succ 0); get) in λs'. x') 0
⟶ reset (let x' = (shift0 (λk. λs. k unit (succ 0)); get) in λs'. x') 0
⟶ (λk. λs. k unit (succ 0)) (λx. reset (let x' = (x; get) in λs'. x')) 0
⟶ (λs. (λx. reset (let x' = (x; get) in λs'. x')) unit (succ 0)) 0
⟶ λx. reset (let x' = (x; get) in λs'. x') unit (succ 0)
⟶ reset (let x' = (unit; get) in λs'. x') (succ 0)
⟶ reset (let x' = get in λs'. x') (succ 0)
⟶* reset (let x' = succ 0 in λs'. x') (succ 0)
⟶ reset (λs'. succ 0) (succ 0)
⟶ (λs'. succ 0) (succ 0)
⟶ succ 0
In pure λ-calculus
We can simulate reset/shift0 in pure
λ-calculus using a whole-program transformation.
Let R be the type of a reset expression.
Inside this expression, every Nat becomes a
(Nat → R) → R. A Nat → R is a
continuation, a function looking for a Nat, so a
(Nat → R) → R takes a continuation of
Nat → R and it returns an R.
If a term has no shifts, it just passes the value of the
term to the continuation:
𝒯⟦x⟧ = λk. k x
𝒯⟦λx. t⟧ = λk. k (λx. 𝒯⟦t⟧)
Application is straightforward as always:
𝒯⟦x1 x2⟧ = x1 x2
The translation of let is:
𝒯⟦let x = t1 in t2⟧ = λk. 𝒯⟦t1⟧ (λx. 𝒯⟦t2⟧ k)
Roughly speaking, the continuation of t1 is
t2, and the continuation of t2 is the
continuation of the let expression.
The above translation, which is called the continuation-passing transform and results in continuation-passing style, is highly important in its own right, and we will look at it more closely in the next page.
The translation of shift0 and reset is
weirdly simple. A shift0 x (where x is a
variable) becomes a function that takes a continuation, passes it to
x, and returns whatever x returns.
𝒯⟦shift0 x⟧ = λk. x k = x
The translation of reset should give the translation of
t an empty continuation:
𝒯⟦reset t⟧ = 𝒯⟦t⟧ (λx. 𝒯⟦x⟧)
References
Asai and Kiselyov, Introduction to Programming with Shift and Reset
Filinski, 1994. Representing Monads. In Proc. POPL.
Materzok and Biernacki, 2012. A Dynamic Interpretation of the CPS Hierarchy. In Proc. APLAS.