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 shift
s, 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.