MIT James Koppel

Gabriel Scherer

Armando Solar-Lezama

Capturing the Future by Replaying the Past Functional Pearl

Keywords: CCS Concepts:, Theory of computation → Control primitives, Functional constructs, •Software and its engineering → Functional languages, Control structures, General programming languages, monads, delimited continuations ACM Reference format:

Delimited continuations are the mother of all monads! So goes the slogan inspired by Filinski's 1994 paper, which showed that delimited continuations can implement any monadic e ect, letting the programmer use an e ect as easily as if it was built into the language. It's a shame that not many languages have delimited continuations.

Luckily, exceptions and state are also the mother of all monads! In this Pearl, we show how to implement delimited continuations in terms of exceptions and state, a construction we call thermometer continuations. While traditional implementations of delimited continuations require some way of "capturing" an intermediate state of the computation, the insight of thermometer continuations is to reach this intermediate state by replaying the entire computation from the start, guiding it using a recording so that the same thing happens until the captured point.

Along the way, we explain delimited continuations and monadic re ection, show how the Filinski construction lets thermometer continuations express any monadic e ect, share an elegant special-case for nondeterminism, and discuss why our construction is not prevented by theoretical results that exceptions and state cannot macro-express continuations.

INTRODUCTION

In the days when mainstream languages have been adopting higher-order functions, advanced monadic e ects like continuations and nondeterminism have held out as the province of the bourgeois programmer of obscure languages. Until now, that is.

Of course, there's a di erence between e ects which are built into a language and those that must be encoded. Mutable state is built-in to C, and so one can write int x = 1; x += 1; int y = x + 1;. Curry 1 is nondeterministic, and so one can write (3 ? 4) * (5 ? 6), which evaluates to all of {15, 18, 20, 24}. This is called the direct style. When an e ect is not built into a language, the monadic, or indirect, style is needed. In the orthodox indirect style, after every use of an e ect, the remainder of the program is wrapped in a lambda. For instance, the nondeterminism example construction and how it combines with thermometer continuations to get arbitrary monadic effects. Section 6 discusses how to optimize the fusion of thermometer continuations with Filinski's construction, while Section 7 provides a few benchmarks showing that thermometer continuations are not entirely impractical. Finally, Section 8 discusses why our construction does not contradict a theoretical result that exceptions and state cannot simulate continuations. We also sketch a correctness proof of replay-based nondeterminism in Appendix A.

WARM-UP: REPLAY-BASED NONDETERMINISM

Nondeterminism is perhaps the rst e ect students learn which is not readily available in a traditional imperative language. This section presents replay-based nondeterminism, a useful specialization of thermometer continuations, and an introduction to its underlying ideas.

When writing the examples in this paper, we sought an impure language with built-in support for exceptions and state, and which has a simple syntax with good support for closures. We hence chose to present in SML. For simplicity, this paper will provide implementations that use global state and are hence not thread-safe. We assume they will not be used in multi-threaded programs.

Nondeterminism provides a choice operator choose such that choose [x1, x2, . . .] may return any of the x i . Its counterpart is a withNondeterminism operator which executes a block that uses choose, and returns the list of values resulting from all executions of the block.

withNondeterminism (fn () ⇒

(choose [2,3,4]) * (choose [5,6])) ( * val it = [10,12,15,18,20,24] : int list * )

In this example, there are six resulting possible values, yet the body returns one value. It hence must run six times. The replay-based implementation of nondeterminism does exactly this: in the rst run, the two calls to choose return 2 and 5, then 2 and 6 in the second, etc. In doing so, the program behaves as if the rst call to choose was run once but returned thrice. We'll soon show exactly how this is done. But rst, let us connect our approach to the one most familiar to Haskell programmers: achieving nondeterminism through monads.

In SML, a monad is any module which implements the following signature (and satis es the monad laws):

signature MONAD = sig type α m val return : α → α m val bind : α m → ( α → β m) → β m end;
Here is the implementation of the list monad in SML: The ListMonad lets us rewrite the above example in monadic style. In the direct style, choose [2,3,4] would return thrice, causing the rest of the code to run thrice. Comparatively, in the monadic style, the rest of the computation is passed as a function to ListMonad.bind, which invokes it thrice.

open ListMonad;

-bind [2,3,4] (fn x ⇒ bind [5,6] (fn y ⇒ return (x * y)))

( * val it = [10,12,15,18,20,24] : int list * )

Let's look at how the monadic version is constructed from the direct one. From the perspective of the invocation choose [2, 3, 4], the rest of the expression is like a function awaiting its result, which it must invoke thrice:

C = (fn ⇒ * choose [5, 6])
This remaining computation is the continuation of choose [2,3,4]. Each time choose [2,3,4] returns, it invokes this continuation. The monadic transformation captured this continuation, explicitly turning it into a function. This transformation captures the continuation at compile time, but it can also be captured at runtime with the call/cc "call with current continuation" operator: if this rst call to choose were replaced with call/cc (fn k ⇒ . . .), then k would be equivalent to C. So, the functions being passed to bind are exactly what would be obtained if the program were instead written in direct style and used call/cc.

This insight makes it possible to implement a direct-style choose operator. The big idea is that, once call/cc has captured that continuation C in k, it must invoke k thrice, with values 2, 3, 4. This implementation is a little verbose in terms of call/cc, but we'll later see how delimited continuations make this example simpler than with call/cc-style continuations.

Like the monadic and call/cc-based implementations of nondeterminism, replay-based nondeterminism invokes the continuation ( ⇒ * choose [5,6]) three times. Since the program (choose [2,3,4] * choose [5,6]) is in direct style, and it cannot rely on a built-in language mechanism to capture the continuation, it does this by running the entire block multiple times, with some bookkeeping to coordinate the runs. We begin our explanation of replay-based nondeterminism with the simplest case: a variant of choose which takes only two arguments, and may only be used once.

The Simple Case: Two-choice nondeterminism, used once

We begin by developing the simpli ed choose2 operator. Calling choose2 (x,y) splits the execution into two paths, returning x in the rst path and y in the second. For example:

-(withNondeterminism2 (fn () ⇒ 3 * choose2 (5, 6))) =⇒ [3 * 5, 3 * 6] =⇒ [15, 18] This execution trace hints at an implementation in which withNondetermism2 calls the block twice, and where choose2 returns the rst value in the rst run, and the second value in the second run. withNondeterminism2 uses a single bit of state to communicate to choose2 whether it is being called in the rst or second execution. As long as the block passed to withNondeterminism2 is pure, with no e ects other than the single use of choose (and hence no nested calls to withNondeterminism2), each execution will have the same state at the time choose is called. [15,18] : int list * )

Less Simple: many-choices non-determinism, used once

If choose expects an α list instead of just two elements, the natural idea is to store, instead of a boolean, the index of the list element to return. However, there is a di culty: at the time where withNondeterminism is called, it doesn't know yet what the range of indices will be.

For a given program with a single choose operator, we'll refer to the list passed to choose as the choice list. Because the code up until the choose call is deterministic, the choice list will be the same every time, so the program can simply remember its length in a piece of state.

Everything the implementation needs to know to achieve nondeterminism -which item to choose on the next invocation, and when to stop -can be captured in two pieces of state: the index to choose from, and the length of the choice list. We call this (index, length) pair a choice index. On the rst run, our implementation knows that it must pick the rst choice (if there is one), but it doesn't know the length of the choice list. Hence, the global state is actually an option of a choice index, which starts at NONE.

type idx = int * int val state : idx option ref = ref NONE
So, for instance, in the program withNondeterminism (fn () ⇒ 2 * choose [1, 2, 3]), the body will be run thrice, with states of NONE, SOME (1, 3), and SOME (2, 3) respectively, instructing choose to select each item from the list.

We de ne some auxiliary functions on indices: to create the rst index in a list, advance the index, and get the corresponding element from the choice list.

fun start _ idx xs = (0, List.length xs) fun next _ idx (k, len) = if k + 1 = len then NONE else SOME (k + 1, len) fun get xs (k, len) = List.nth (xs, k)
We can now write the single-use choose function for arbitrary lists. An empty list aborts the computation with an exception. Otherwise, it looks at the state. If it is already set to some index, it returns the corresponding element. Otherwise it initializes the state with the rst index and returns this element. The withNondeterminism function loops through every choice index, accumulating the results into a list. It returns an empty value if the choice list is empty.

fun withNondeterminism f = let val v = [f ()] handle Empty ⇒ [] in case !state of NONE ⇒ v | SOME i ⇒ case next _ idx i of NONE ⇒ v | SOME i' ⇒ (state := SOME i'; v @ withNondeterminism f) end
Here's withNondeterminism in action: [2,4,6] : int list

-withNondeterminism (fn () ⇒ 2 * choose []); val it = [] : int list -withNondeterminism (fn () ⇒ 2 * choose [1, 2, 3]); val it =

Several calls to choose

In the previous implementation, a single choice index was su cient to track all the choices made. To track several uses of choose in the body of withNondeterminism, we need a list of choice indices.

We can view a nondeterministic computation as a tree, where each call to choose is a node, and each path corresponds to a sequence of choices. Our program must nd every possible result of the nondeterministic computation -it must nd all leaves of the tree. The implementation is like a depth rst search, except that it must replay the computation (i.e.: start from the root) once for each leaf in the tree. For example:

withNondeterminism (fn () ⇒ if choose [true,false] then choose [5,6] else choose [7,8,9])
There are ve paths in the execution tree of this program. In the rst run, the two calls to choose return (true, 5). In the second, they return (true, 6), followed by (false, 7), (false, 8) and (false, 9). Each path is identi ed by the sequence of choice indices of choices made during that execution, which we call a path index. Our algorithm is built on a fundamental operation: to select a path index, and then execute the program down that path of the computation tree. To do this, it partitions the path index into two stacks during execution: the past stack contains the choices already made, while future contains the known choices to be made. Figure 1 depicts the execution tree for the last example, showing values of past and future at di erent points in the algorithm. To make them easier to update, path indices are stored as a stack, where the rst element of the list represents the last choice to be made. For instance, the rst path, in which the calls to choose return true and 5, has path index [(0, 2), (0, 2)], and the next path, returning true and 6, has path index [(1, 2), (0, 2)]. These are shown in Figures 1a and1b. The next _ path function inputs a path index, and returns the index of the next path.

fun next _ path [] = [] | next _ path (i :: is) = case next _ idx i of SOME i' ⇒ i' :: is | NONE ⇒ next _ path is
When execution reaches a call to choose, it reads the choice to make from future, and pushes the result into the past. What if the future is unknown, as it will be in the rst execution to reach a given call to choose? In this case, choose picks the rst choice, and records it in past. Figure 1c depicts this scenario for the rst time execution enters the else branch of our example. The execution of a path in the computation tree ends when a value is returned. At this point, future is empty (all known choose calls have been executed), and past contains the complete path index.

The withNondeterminism function is exactly the same as in Section 2.2, except that instead of updating the state to the next choice index, it computes the next path index, which becomes the future of the next run:

fun withNondeterminism f = let val v = [f ()] handle Empty ⇒ [] val next _ future = List.rev (next _ path (!past)) in past := []; future := next _ future; if !future = [] then v else v @ withNondeterminism f end
When withNondeterminism terminates, it must be the case that both (!past) = [] and (!future) = [],

which allows to run it again.

-withNondeterminism (fn () [3,12,102,4,22,202,5,32,302] : int list

⇒ if choose [true, false] then choose [1, 2] else choose [3, 4]); val it = [1,2,3,4] : int list -withNondeterminism (fn () ⇒ 2 + choose [1, 2, 3] * choose [1, 10, 100]); val it =
There is still one thing missing: doing nested calls to withNondeterminism would overwrite future and past, so nested calls return incorrect results.

-withNondeterminism (fn ()

⇒ if choose [true, false] then withNondeterminism (fn () ⇒ choose [1, 2]) else []); val it = [[1,1,2]] : int list list

Supporting nested calls

The nal version of withNondeterminism supports nested calls by saving the current values of past and future onto a stack before each execution, and restoring them afterwards. No change to choose is needed. The previous section showed a trick for implementing direct-style nondeterminism in deterministic languages. Now, we delve to the deeper idea behind it, and surface the ability to generalize from nondeterminism to any monadic e ect. We now examine how replay-based nondeterminism stealthily manipulates continuations. Consider evaluating this expression:

val e = withNondeterminism (fn () ⇒ choose [2,3,4] * choose [5, 6])
Every subexpression of e has a continuation, and when it returns a value, it invokes that continuation. After the algorithm takes e down the rst path and reaches the point T = 2 * choose [5, 6], this second call to choose has continuation C = ( ⇒ 2 * ).

The choose function must invoke this continuation twice, with two di erent values. But C is not a function that can be repeatedly invoked: it's a description of what the program does with a value after it's returned, and returning a value causes the program to keep executing, consuming the continuation. So choose invokes this continuation the rst time normally, returning 5. To copy this ephemeral continuation, it re-runs the computation until it's reached a point identical to T, evaluating that same call to choose with a second copy of C as its continuation -and this time, it invokes the continuation with 6. So, the rst action of the choose operator is capturing the continuation. And what happens next? The continuation is invoked once for each value, and the results are later appended together. We've already seen another operation that invokes a function once on each value of a list and appends the results: the ListMonad.bind operator. Figure 2 depicts how applying ListMonad.bind to the continuation produces direct-style nondeterminism.

E E 1 E 2 E 3 [1,2,3] [ ] , , > >= = flatten
So, replay-based nondeterminism is actually a fusion of two separate ideas:

(1) Capturing the continuation using replay (2) Using the captured continuation with operators from the nondeterminism monad

In Section 4, we extract the rst half to create thermometer continuations, our replay-based implementation of delimited control. The second half -using continuations and monads to implement any e ect in direct style -is Filinski's construction, which we explain in Section 5. These produce something more ine cient than the replay-based nondeterminism of Section 2, but we'll show in Section 6 how to fuse them together into something equivalent.

THERMOMETER CONTINUATIONS: REPLAY-BASED DELIMITED CONTROL

In the previous section, we explained how the replay-based nondeterminism algorithm actually hides a mechanism for capturing continuations. Over the the remainder of this section, we extract out that mechanism, developing the more general idea of thermometer continuations. But rst, let us explain the variant of continuations that our mechanism uses: delimited continuations.

What is delimited control?

When we speak of "the rest of the computation, " a natural question is "until where?" For traditional continuations, the answer is: until the program halts. This crosses all abstraction boundaries, making these "undelimited continuations" di cult to work with. Another answer is: until some programmer-speci ed "delimiter" or "reset point." This is the answer of delimited continuations, introduced by Felleisen,4 which only represent a pre x of the remaining computation. Just as call/cc makes continuations rst class, allowing a program to modify its continuation to implement many di erent global control-ow operators, the shift and reset constructs make delimited continuations rst-class, and can be used to implement many local control-ow operators.

In the remainder of this section, we'll use the notation E[x] to denote plugging value x into evaluation context E. We will also write (t) for the expression reset (fun () ⇒ t). These notations makes it easy to give the semantics for shift and reset.

If the body of a reset reaches a value v, then this value is returned to the outside context.

E 1 [ (v)] =⇒ E 1 [v]
Capturing the Future by Replaying the Past 10:11 On the other hand, if the evaluation inside a reset reaches a call to shift, then the argument of shift is invoked with a delimited continuation k corresponding to the entire evaluation context until the closest reset call:

E reset boundary shift (λk.M) reset boundary M x E k = λx
E 1 [ (E 2 [shift (fn k ⇒t)])] =⇒ E 1 [ (let fun k x = (E 2 [x]) in t)]
where E 2 does not contain contexts of the form (E). Figure 3 depicts this evaluation.

While call/cc captures an undelimited continuation that goes until the end of the program, shift only captures the continuation up to the rst enclosing reset -it is delimited.

Notice that both the evaluation of shift's argument t and the body of its continuation k are wrapped in their own reset calls; this a ects the (quite delicate) semantics of nested calls to shift. There exists other variants of control operators that make di erent choices here. 5 If the continuation k passed to shift is never called, it means that the captured context E 2 is discarded and never used; this use of shift corresponds to exceptions.

-reset (fn ()

⇒ [1, 2] @ shift (fn k ⇒ [3, 4])) =⇒ reset (fn () ⇒ let fun k = x = [1, 2] @ x in reset (fn () ⇒ [3, 4])) =⇒ [3, 4]
On the other hand, using a single continuation multiple times can be used to emulate choose-style non-determinism.

-reset (fn ()

⇒ 3 * shift (fn k ⇒ [k 2, k 3, k 4])); =⇒ reset (fn () ⇒ let fun k x = 3 * x in reset (fn () ⇒ [k 2, k 3, k 4]))
=⇒ [6, 9, 12] It is not easy to give precise polymorphic types to shift and reset; in this work, we make the simplifying assumption that they return a xed "answer" type ans, as encoded in the following SML signature.

signature CONTROL = sig type ans val reset : (unit → ans) → ans val shift : (( α → ans) → ans) → α end;
See the tutorial of Asai and Kiselyov6 for a more complete introduction to delimited control operators.

Baby Thermometer Continuations

Programming with continuations requires being able to capture and copy an intermediate state of the program. We showed in Section 3 that replay-based nondeterminism implicitly does this by replaying the whole computation, and hence needs no support from the runtime. We shall now see how to do this more explicitly.

This section presents a simpli ed version of thermometer continuations. This version assumes the reset block only contains one shift, which invokes its passed continuation 0 or more times. It also restricts the body of shift and reset to only return integers. Still, this implementations makes it possible to handle several interesting examples.

Whenever we evaluate a reset, we'll store its body as cur _ expr:

exception Impossible

val cur _ expr : (unit → int) ref = ref (fn () ⇒ raise Impossible)
fun reset f = (cur _ expr := f;

( * rest of function given later * ) )

Now, let F be some function containing a single shift, i.e.:

F = (fn () ⇒ E[shift (fn k ⇒ t))],
where E is a pure evaluation context (no additional e ects). Then the main challenge in evaluating reset F is to capture the continuation of the shift as a function, i.e.: to obtain a

C = (fn x ⇒ E[x]).
Like the future stack in replay-nondeterminism that controls the action of choose, our trick is to use a piece of state to control shift.

val state : int option ref = ref NONE
Suppose we implement shift so that (state := SOME x; shift f) evaluates to x, regardless of f. Now consider a function which sets the state, and then replays the reset body, e.g.: a function k = (fn x ⇒ (state := SOME x; (!cur _ expr) ())). Because E is pure, the e ectful code state := SOME x commutes with everything in E. This means the following equivalences hold:

k y ≡ (fn x ⇒(state := SOME x; (!cur _ expr) ())) y (by definition of k) ≡ (state :=SOME y; E[shift (fn k ⇒ t)]) ≡ E[state :=SOME y; shift (fn k ⇒ t)] (E commutes with state) ≡ E[y] (shift property)
which means that k is exactly the continuation we were looking for! We are now ready to de ne reset and shift. reset sets the cur _ expr to its body, and resets the state to NONE. It then runs its body. For the cases like reset (fn () ⇒ 1 + shift (fn () ⇒ 2)), where the shift aborts the computation and returns directly to reset, the shift will raise an exception and reset will catch it. This gives us the de nition of shift f: if shift f is being invoked from running the continuation, then state will not be NONE, and it should return the value in state. Else, it sets up a continuation as above, runs f with that continuation, and passes the result to the enclosing reset by raising an exception:

fun shift f = case !state of SOME x ⇒ x | NONE ⇒ let val expr = !cur _ expr fun k x = (state := SOME x; expr ()) val result = f k in raise (Done result) end
We can now evaluate some examples with basic delimited continuations:

-reset (fn () ⇒ 2 * shift (fn k ⇒ 1 + k 5)) val it = 11 : int -reset (fn () ⇒ 1 + shift (fn k ⇒ (k 1) * (k 2) * (k 3))) val it = 24 : int
The de nitions given here lend themselves to a simple equational argument for correctness. Consider an arbitrary reset body with a single shift and no other e ects. It reduces through these equivalences:

E 1 [reset (fn () ⇒E 2 [shift (fn k ⇒t)])] =⇒ E 1 [E 2 [raise (Done (let fun k x = (state :=SOME x; E 2 [shift (fn k ⇒t)]) in t))] handle (Done x ⇒ x)] ≡ E 1 [let fun k x = (state :=SOME x; E 2 [shift (fn k ⇒t)]) in t] ≡ E 1 [let fun k x = (E 2 [state :=SOME x; shift (fn k ⇒t)]) in t] ≡ E 1 [let fun k x = (E 2 [x]) in t]
Except for the nested reset's, which do nothing in the case where there is only one shift, this is exactly the same as the semantics we gave in Section 4.1.

General Thermometer Continuations

Despite its simplicity, the restricted delimited control implementation of Section 4.2 already addressed the key challenge: capturing a continuation as a function. Just as we upgraded the single-choice nondeterminism into the full version, we'll now upgrade that implementation into something that can handle arbitrary code with delimited continuations. We explain this process in ve steps:

(1) Allowing answer types other than int (2) Allowing multiple sequential shift's

(3) Allowing di erent calls to shift to return di erent types (4) Allowing nested shift's (5) Allowing nested reset's

We'll go through each in sequence.

Di erent answer types. As mentioned in Section 4.1, it's not easy to give precise polymorphic types to shift and reset. What can be done is to de ne them for any xed answer type. An ML Multiple sequential shift's. We will now generalize the construction of the previous section to multiple sequential shift's. We do this similarly to what we did for replay nondeterminism, where we generalized the single choice index to a sequence of indices representing a path in the computation tree, and created the operation of sending execution along a certain path in the computation tree. This time, we generalize the simple state of the previous section which controls the return value of a shift, into a sequence of return values of sequential shift's, making execution proceed in a certain fashion. Consider this example:

-reset (fn () ⇒ shift (fn k ⇒ 1 + k 2) * shift (fn k' ⇒ 1 + k' 3)) =⇒ 8
Suppose the state commands the rst shift to return x (call this Execution A). Then replaying the computation evaluates to x * shift (fn k'⇒ 1 + k'3) -exactly the continuation of the rst shift. Meanwhile, if the state commands the rst shift to return 2 (Execution B), and the second to return x, then replay will evaluate to 2 * x -this is the continuation of the second shift! So, this state is a sequence of commands called frames, where each command instructs a shift to return a value (we'll soon add another kind of command). The sequence of values to be returned by future calls to shift is called the future, so that Execution A was described by the future

[RETURN x],
and Execution B by [RETURN 2, RETURN x]. This means that the continuation could be described by everything up to the RETURN x, namely the empty list [] and [RETURN 2], respectively. This gives us our de nition of a thermometer continuation: De nition 4.1. A thermometer continuation for a continuation C is a pair of a list and a computation, (s, block), so that for appropriate change _ state and run, the code change _ state x s; run block evaluates to

C[x].
Or, in code:

datatype frame = RETURN of int type thermo _ cont = (unit → ans) * frame list
The key operation of thermometer continuations is to run the computation using the associated future, making execution proceed to a certain point. Just as in replay nondeterminism, during execution, each item is moved from the future to the past as it's used: Di erent types for di erent shift's. A future stores the values to be returned by di erent shift's. When these shift's are used at di erent types, the future will hence be a list of heterogeneous types, which poses typing problems. Replay-nondeterminism dodged this problem by storing an index into the choice list instead of the value to return itself. That solution does not work here, where the value to be returned may be from a distant part of the program. In an expression shift (fn k ⇒ t), the continuation k will only be used to pass values to future invocations of that same shift, which has the correct type. However, we cannot explain this to the ML type system. Instead, we use a hack, a universal type u in which we assume that all types can be embedded and projected back: We use this universal type to de ne the real frame type below. Nested shift's. With sequential shift's, bringing execution to the desired point only required replacing shift's with a return value. But with nested shift's, the continuation to be captured may be inside another shift, and so replay will need to enter it. Consider this example:

val past : frame list ref = ref [] val future : frame list ref = ref []
-1 + reset (fn () ⇒ 2 + shift (fn k ⇒ 3 * shift (fn l ⇒ l (k 10)))) val it = 37 : int
The delimited continuation of the second shift is C = ( ⇒ 3 * ). In replay, execution must enter the body of the rst shift, but replace the second shift with a value. So, in addition to RETURN frames, there must be another kind of frame, which instructs replay to enter the body of a shift. We are now ready to begin giving the nal implementation of thermometer continuations. As before, shift will pass its value to reset by raising an exception.

exception Done of ans

The key operation is to play a computation according to a known future. We implement this in the run _ with _ future function, which both does that, and establishes a new reset boundary. When doing so, it must save and restore the current state. This is similar to the withNondeterminism version with nesting, with the added management of cur _ expr. The reset we expose to the user is just a specialized instance of run _ with _ future, running with an empty future: Capturing the Future by Replaying the Past 10:17

fun reset f = run _ with _ future f [] Proc. ACM Program. Lang.,
Finally, shift is similar to choose. The easy case is when the next item in the future is a return frame; it just has to return the commanded value.

fun shift f = case pop future of SOME (RETURN v) ⇒ (push past (RETURN v); Universal.from _ u v) | (NONE | SOME ENTER) ⇒ let val new _ future = List.rev (!past) val our _ expr = !cur _ expr fun k v = run _ with _ future our _ expr (new _ future @ [RETURN (Universal.to _ u v)]) val () = push past ENTER val result = f k in raise (Done result) end
The two other cases are more delicate. The ENTER case corresponds to the case where execution must enter the shift body -the function f. The case NONE is when the future is unknown, but, in that case, it should do exactly the same: enter the shift body.

It records this decision by pushing an ENTER frame into the past, and prepares a function k that invokes the thermometer continuation de ned by (!cur _ expr, List.rev (!past)). It invokes the thermometer continuation by appending a new value to the future, and then calling run _ with _ future. The body of the shift executes with this k; if it terminates normally, it raises an exception to pass the result to the enclosing reset.

-1 + reset (fn () ⇒ 2 + shift (fn k ⇒ 3 * shift (fn l ⇒ l (k 10)))) =⇒ 37

What language features does this need?

Thermometer continuations clearly require exceptions and state, but the implementation of this section uses a couple other language features as well. Here, we discuss why we chose to use them, and ideas for alternatives.

Why we need Unsafe.cast for universal types. Our implementation of thermometer continuations uses Unsafe.cast, as in the original presentation of Filinski,8 but shift and reset are carefully designed so that these casts never fail. Similarly, we believe that our use of Unsafe.cast is actually type-safe: we only inject values into the universal type at the call site of an e ectful function, and only project values back in the same call site they are coming from.

In his follow-up paper, 9 Filinski was able to upgrade these de nitions to a type-safe implementation of the universal type.10 Unfortunately, we cannot just replace our implementation with a safe one.

Safe implementations of universal types cannot provide a pair of uniform polymorphic embedding and projection functions (α →u) * (u →α) -this is inherently unsafe. Instead, they provide a function that, on each call, generates a new embedding/projection pair for a given type:

unit →((α→u) * (u →α)). They all use a form of dynamic generativity, such as the allocation of new memory, or the creation of a new exception constructor.

These implementations do not work properly with our replay-based technique: each time a computation is replayed, a fresh embedding/projection pair is generated, and our implementation then tries to use the projection of a new pair on a value (of the same type) embedded by an old pair, which fails at runtime.

It may be possible to integrate the generation of universal embeddings within our replay machinery, to obtain safe implementations, but we left this delicate enhancement to future work.

Garbage Collection and Closures. This implementation uses closures heavily. This is a small barrier to someone implementing thermometer continuations in C, as they must encode all closures as function pointer/environment pairs. However, there's a bigger problem: how to properly do memory management for these environments? A tempting idea is to release all environments after the last reset terminates, but this doesn't work: the captured continuations may have unbounded lifetime, as exempli ed by the state monad implementation in Section 5.4.

The options to solve this problem are the same as when designing other libraries with manual memory management: either make the library specialized enough that it can guess memory needs, or provide extra APIs so the programmer can indicate them. For example, one possibility for the latter is to allocate all closures for a given reset block in the same arena. At some point, the programmer knows that the code will no longer need any of the continuations captured within the reset. The programmer invokes an API to indicate this, and all memory in that arena is released.

ARBITRARY MONADS

In 1994, Filinski showed how to use delimited continuations to express any monadic e ect in direct style. 11 In this section, we hope to convey an intuition for Filinski's construction, and also discuss what it looks like when combined with thermometer continuations. The code in this section comes almost verbatim from Filinski. This section is helpful for understanding the optimizations of Section 6, in which we explain how to fuse thermometer continuations with the code in this section.

Monadic Reflection

In SML and Java, there are two ways to program with mutable state. The rst is to use the language's built-in variables and assignment. The second is to use the monadic encoding, programming similar to how a pure language like Haskell handles mutable state. A stateful computation is a monadic value, a pure value of type s → (a, s).

These two approaches are interconvertible. A program can take a value of type s → (a, s) and run it, yielding a stateful computation of return type a. This operation is called reflect. Conversely, it can take a stateful computation of type a, and reify it into a pure value of type s → (a, s). Together, the reflect and reify operations give a correspondence between monadic values and e ectful computations. This correspondence is termed monadic re ection. Fillinski showed how, using delimited control, it is possible to encode these operations as program terms.

The reflect and reify operations generalize to arbitrary monads. Consider nondeterminism, where a nondeterministic computation is either an e ectful computation of type α , or a monadic value of type α list. Then the reflect operator would take the input [1, 2, 3] and nondeterministically return 1, 2, or 3 -this is the choose operator from Section 2). So reify would take a computation that nondeterministically returns 1, 2, or 3, and return the pure value [1, 2, 3] -this is withNondeterminism.

Capturing the Future by Replaying the Past 10:19 So, for languages which natively support an e ect, reflect and reify convert between e ects implemented by the semantics of the language, and e ects implemented within the language. Curry is a language with built-in nondeterminism, and it has these operators, calling them anyOf and getAllValues. SML does not have built-in nondeterminism, but, for our previous example, one can think of the code within a withNondeterminism block as running in a language extended with nondeterminism. So, one can think of the construction in the next section as being able to extend a language with any monadic e ect.

In SML, monadic re ection is given by the following signature:

signature RMONAD = sig structure M : MONAD val reflect : α M.m → α val reify : (unit → α ) → α M.m end;

Monadic Reflection through Delimited Control

Filinski's insight was that the monadic style is similar to an older concept called continuationpassing style. We can see this by revisiting an example from Section 2. Consider this expression:

withNondeterminism (fun () ⇒ (choose [2,3,4]) * (choose [5,6]))
It is transformed into the monadic style as follows: The rst call to choose has continuation fn ⇒ * (choose [5,6]). If x is the value returned by the rst call to choose, the second has continuation fn ⇒ x * . These continuations correspond exactly to the functions bound in the monadic style. The monadic bind is the "glue" between a value and its continuation. Nondeterministically choosing from [2,3,4] wants to return thrice, which is the same as invoking the continuation thrice, which is the same as binding to the continuation. So, converting a program to monadic style is quite similar to converting a program to this "continuation-passing style. " Does this mean a language that has continuations can program with monads in direct style? Filinski answers yes.

The de nition of monadic re ection in terms of delimited control is short. The overall setup is as follows:

functor If reflect uses shift, then reify uses reset to delimit the e ects implemented through shift. This implementation requires use of type casts, because reset is monomorphized to return a value of type Universal.u m. Without these type casts, reify would read fun reify t = C.reset (fn () ⇒ M.return (t ()))

Because of the casts, the actual de nition of reify is slightly more complicated:

fun reify t = M.bind (C.reset (fn () ⇒ M.return (Universal.to _ u (t ())))) (M.return o Universal.from _ u)

Example: Nondeterminism

Using this general construction, we immediately obtain an implementation of nondeterminism equivalent to the one in Section 2 by using ListMonad (de ned in Section 2). [21,20,28] : int list * )

It's worth thinking about how this generic implementation executes on the example, and contrasting it with the direct implementation of Section 2. The direct implementation executes the function body 6 times, once for each path of the computation. The generic one executes the function body 10 times (once with a future stack of length 0, 3 times with length 1, and 6 times with length 2).

In the direct implementation, choose will return a value if it can. In the generic one, choose never returns. Instead, it invokes the thermometer continuation, causes the desired value to be returned at the equivalent point in the computation, and then raises an exception containing the nal result. So, 4 of those times, it could just return a value rather than replaying the computation. This is the idea of one of the optimizations we discuss in Section 6. This, plus one other optimization, let us derive the direct implementation from the generic one.

Example: State monad

State implemented through delimited control works di erently from SML's native support for state. Let's take a look at how this works, starting with the example reify (fn () ⇒ 3 * get ()).

- The get in reify (fn () ⇒ 3 * get ()) suspends the current computation, causing the reify to return a function which awaits the initial state. Once invoked with an initial state, it resumes the computation (multiplying by 3). What does reify (fn () ⇒ (tick (); get ())) do? The call to tick () again suspends the computation and awaits the state, as we can see from its expansion shift (fn k ⇒ fn s ⇒ k () (s+1)). Once it receives s, it resumes it, returning () from tick. The call to get suspends the computation again, returning a function that awaits a new state; tick supplies s+1.

Think for a second about how this works when shift and reset are implemented as thermometer continuations. The get, put, and tick operators do not communicate by mutating state. They communicate by suspending the computation, i.e.: by raising exceptions containing functions. So, although the implementation of state in terms of thermometer continuations uses SML's native support for state under the hood, it only does so tangentially, to capture the continuation.

OPTIMIZATIONS

Section 5.3 compared the two implementations of nondeterminism, and found that the generic one using thermometer continuations replayed the computation gratuitously. Thermometer continuations also replay the program in nested fashion, consuming stack space. In this section, we sketch a few optimizations that make monadic re ection via thermometer continuations less impractical, and illustrate the connections between the two implementations of nondeterminism.

All optimizations in this section save memoization require fusing thermometer continuations with monadic re ection. So, instead of using the de nition of reflect in terms of shift given in Section 5.2, the require de ning a single combined reflect operator.

CPS-bind: Invoking the Continuation at the Top of the Stack

The basic implementation of thermometer continuations wastes stack space. When there are multiple shift's, execution will reach the rst shift, then replay the function and reach the second shift, then replay again, etc, all the while letting the stack deepen. And yet, at the end, it will raise an exception that discards most of the computation on the stack. So, the implementation could save a lot of stack space by raising an exception before replaying the computation.

So, when a program invokes a thermometer continuation, it will need to raise an exception to transfer control to the enclosing reset, and thereby signal reset to replay the computation. While the existing Done exception signals that a computation is complete, it can do this with a second kind of exception, which we call Invoke.

However, the shift and reset functions do not invoke a thermometer continuation: the body of the shift does. In the case of monadic re ection, this is the monad's bind operator. Raising an Invoke exception will discard the remainder of bind, so it must somehow also capture the continuation of bind. We can do this by writing bind itself in the continuation-passing style, i.e.: with the following signature:

type (β, γ) cont = (β → γ) → γ val bind : α m → ( α → (β m, γ) cont) → (β m, γ) cont
The supplementary material contains code with this optimization, and uses it to implement nondeterminism in a way that executes more similarly to the direct implementation. We give here some key points. Here's what the CPS'd bind operator for the list monad looks like:

fun bind [] f d = d [] | bind (x :: xs) f d = f x (fn a ⇒ bind xs f (fn b ⇒ d (a @ b)))
When used by reflect, f becomes a function that raises the Invoke exception, transferring control to the enclosing reset, which then replays the entire computation, but at the top level. The continuations of the bind get nested in a manner which is harder to describe, but ultimately get evaluated at the very end, also at the top level. So the list appends in d (a @ b) actually run at the top level of the reset, similar to how, in direct nondeterminism, it is the outer call to withNondeterminism that aggregates the results of each path. While this CPS-monad optimization as described here can be used to implement many monadic e ects, it cannot be used to implement all of them, nor general delimited continuations. Consider the state monad from Section 5.4: bind actually returns a function which escapes the outer reify. Then, when the program invokes that function and it tries to invoke its captured thermometer continuation, it will try to raise an Invoke exception to transfer control to its enclosing reify, but there is none. This CPS-monad optimization as described does not work if the captured continuation can escape the enclosing reset. With more work, it could use mutable state to track whether it is still inside a reset block, and then choose to raise an Invoke exception or invoke the thermometer continuation directly.

Direct Returns

In our implementation, a reset body E[reflect (return 1)] expands into

E[raise (Done (E[1] handle (Done x ⇒ x)))]
So, the entire computation up until that reflect runs twice. Instead of replaying the entire computation, that reflect could just return a value. E[reflect (return 1)] could expand into E [1].

The expression reflect (return 1) expands into shift (fn k ⇒ bind (return 1) k). By the monad laws, this is equivalent to shift (fn k ⇒ k 1). Tail-calling a continuation is the same as returning a value, so this is equivalent to 1. So, it's the tail-call that allows this instance of reflect to return a value instead of replaying the computation.

Implementing the direct-return optimization is an additional small tweak to the CPS-bind optimization. We duplicate the second argument of bind, resulting in two function arguments that expect inputs at type α .

val bind : α m → ( α → (β m) cont) → ( α → (β m) cont) → (β m → γ) → γ
To the implementor of bind, we ask that the rst function be invoked on the rst value of type α (if any) extracted out of the α m output, and the second be used on all later values.

In our implementation of reflect, we use this more exible bind type for optimization. The second function argument we pass raises an Invoke exception, as described in Section 6.1. The rst function returns a value directly, after updating the internal state of the thermometer. So, the rst time bind invokes the continuation, it may do so by directly returning a value. Thereafter, it must invoke the continuation by explicitly raising an Invoke exception.

The bind operator for the list monad never performs a tail-call (it must always wrap the result in a list), but, after converting it to CPS, it always performs a tail-call. So this direct-return optimization combines well with the previous CPS-monad optimization. Indeed, applying them both transforms the generic nondeterminism of Section 5.3 into the direct nondeterminism of Section 2. In Section 7, we show benchmarks showing that this actually gives an implementation of nondeterminism as fast as the code in Section 2.

In the supplementary material, we demonstrate this optimization, providing optimized implementations of nondeterminism (list monad) and failure (maybe monad).

Memoization

While the frequent replays of thermometer continuations can interfere with any other e ects in a computation, it cannot interfere with observationally-pure memoization. Memoizing nested calls to reset can save a lot of computation, and any expensive function can memoize itself without integrating into the implementation of thermometer continuations.

PERFORMANCE NUMBERS

The idea of invoking continuations by replaying the past at rst seems woefully impractical. In fact, it performs surprisingly well in a diverse set of benchmarks. This shows that one may seriously consider using this approach to conveniently write non-performance-critical applications in a runtime (OCaml, Java, Javascript...) that does not provide control operators.

Working code for all benchmarks is available from https://github.com/jkoppel/thermometer-continuations.

The worst case

The overhead of replaying pure computations depends on the ratio, in an e ectful program, of computation time spent in pure and impure computations, and in the branching structure of the computation tree.

The worst case is when a program runs a costly pure computation, followed by large branching of an e ect -causing many replays. The overhead may be arbitrarily large; for example, replay-based or thermometer-based implementations cause a 10x slowdown on the following program: ,2,3,4,5,6,7,8,9] in

withNondeterminism (fun () ⇒ let val v = long _ pure _ computation () in val i = choose [0,1
(i, v) end )

Search-heavy programs: N-queens

The benchmark NQUEENS is the problem of enumerating all solutions to the n-queens problem. It is representative of search programs where most of the time is spent in backtracking search, and we decided to test our approach against a wide variety of alternative implementations to get a sense of the replay overhead on search-heavy non-deterministic programs. Table 1 reports the times for each implementation for di erent n.

Numbers in this section were collected using SML/NJ v110.82, OCaml 4.06, MLton 2018-02-07, the experimental OCaml-multicore runtime for OCaml 4.02.2 (dev0), and GNU Prolog 1.4.4, on a Lenovo T440s with an 1.6GHz Intel Core i5 processor.

All implementations are exponential, and the slope (the exponential coe cient) is essentially the same for all implementations: for a given node in the computation tree, the overhead of replaying the past is proportional to its depth, which is bounded by n, and this bounded multiplicative slowdown factor becomes a lo (n) additive overhead in log-scale.

The generic thermometer-based implementation (using Filinski's construction with replaybased shift and reset) is noticeably slower than the simpler replay-based implementation of nondeterminism. On the other hand, the optimizations described in Section 6 su ce to remove the additional overhead: optimized thermometers are just as fast as replay-nondeterminism.

We benchmarked Filinski's construction using the delimcc library,12 which is an implementation of delimited control doing low-level stack copying; it is comparable in speed to the generic thermometers. This is a nice result even though delimcc is known not to be competitive with control operators in runtimes designed to make them e cient.

We also wrote a version of the benchmark using the e ect handlers provided by the experimental Multicore OCaml runtime. 1314 The performance is better than our replay-based non-determinism (or optimized thermometers), but in the same logarithmic ballpark.

In contrast, in our SML benchmarks, Filinski's construction using the native call/cc of SML/NJ is signi cantly faster than replay-based approaches, closer to the indirect-style baseline. We explain this by the fact that the SML/NJ compilation strategy and runtime has made choices (call frames on the heap) that give a very e cient call/cc, but add some overhead (compared to using the native stack) to general computation. Indeed, in absolute time, OCaml's indirect baseline is about 60% faster than the SML/NJ baseline, and SML/NJ's call/cc approach is actually only 30% faster than OCaml's best replay-based implementations, and slower than the Multicore-OCaml implementation.

OCaml times

10 11 12 13 Indirect 0.007s 0.037s 0.213s 1.308s Replay 0.017s 0.101s 0.597s 3.768s Therm. 0.041s 0.221s 1.347s 8.621s Therm. Opt. 0.019s 0.111s 0.65s 3.924s Filinski (Delimcc) 0.035s 0.185s 1.236s 14.412s E . Handlers (Multicore OCaml) 0.019s 0.092s 0.509s 2.81s SML times 10 11 12 13 Indirect 0.011s 0.064s 0.36s 2.166s Replay 0.029s 0.164s 1.051s 6.562s Therm.

0.06s 0.344s 2.152s 14.305s Therm. Opt.

0.03s 0.173s 1.151s 6.793s Filinski (Call/cc) 0.015s 0.079s 0.493s 2.941s

MLton times 10 11 12 13 Indirect 0.007s 0.031s 0.187s 1.129s Replay 0.016s 0.099s 0.637s 4.295s Finlinski (Call/cc) 0.078s 0.399s 2.138s 12.62s

Prolog times 10 11 12 13 Prolog search (GNU Prolog) 0.165s 0.614s 3.307s 20.401s To validate this hypothesis, we measured the performance of MLton on the benchmarks that it supports (not thermometers, which require a working Unsafe.cast; see Section 4.4). The performance results for the indirect and replay-nondeterminism version are extremely similar to OCaml's, but the call/cc version is much slower than the one of SML/NJ, similar to the performances of delim/cc; on MLton, our replay-based technique is the best direct-style implementation of non-determinism.

Finally, we wrote a Prolog version of N-queens (using Prolog's direct-style backtracking search, not constraint solving). We were surprised to nd out that it is slower than all other implementations. For N=13, GNU Prolog takes around 20s, while our slowest ML versions run in 14s. Prolog may have e cient support for backtracking search, but it seems disadvantaged by being a dynamically-typed language without an aggressive JIT implementation.

Our conclusion is that for computations dominated by nondeterministic search, replay-based approaches are surprisingly practical: they o er reasonable performances compared to other approaches to direct-style nondeterminism that are considered practical.

More benchmarks

More benchmarks in di erent monads are included in Appendix B.

BUT ISN'T THIS IMPOSSIBLE?

In a 1990 paper, Matthias Felleisen presented formal notions of expressibility and macro-expressibility of one language feature in terms of others, along with proof techniques to show a feature cannot be expressed. 15 Hayo Thielecke used these to show that exceptions and state together cannot macro-express continuations. 16 This is concerning, because, at rst glance, this is exactly what we did.

First, a quick review of Felleisen's concepts: Expressibility and macro-expressibility help de ne what should be considered core to a language, and what is mere "syntactic sugar." An expression is a translation from a language L containing a feature F to a language L without it which preserves program semantics. A key restriction is that an expression may only rewrite AST nodes that implement F and the descendants of these nodes. So, an expression of state may only rewrite assignments, dereferences, and expressions that allocate reference cells. Whle there is a wholeprogram transformation that turns stateful programs into pure programs, namely by threading a "state" variable throughout the entire program, this transformation is not an expression. A macro-expression is an expression which may rewrite nodes from F , but may only move or copy the children of such nodes (technically speaking, it must be a term homomorphism). A classic example of a macro-expression is implementing the += operator in terms of normal assignment and addition. A classic example of an expression which is not a macro-expression is desugaring for-loops into while-loops (it must dig into the loop body and modify every continue statement). Another one is implementing Java finally blocks (which need to execute an action after every return statement).

It turns out that Thielecke's proof does not immediately apply. First, it concerns call/cc-style continuations rather than delimited continuations. This seems like a minor limitation, since delimited continuations can be use to implement call/cc. But the second reason is more fundamental.

Thielecke's proof is based on showing that, in a language with exceptions and state but not continuations, all expressions of the following form with di erent j are operationally equivalent:

Capturing the Future by Replaying the Past 10:27

R j = λ f .((λx .λ .(f 0; x := ! ; := j; !x))(ref 0)(ref 0))
The intuition behind this equivalence is that the two reference cells are allocated locally and then discarded, and so the value stored in them can never be observed. However, with continuations, on the other hand, f could cause the two assignments to run twice on the same reference cells.

This example breaks down because it cannot be expressed in our monadic re ection framework as is. The monadic re ection framework assumes there are no other e ects within the program other than the ones implemented via monadic re ection. To write the R j using thermometer continuations and monadic re ection, the uses of ref must be changed from the native SML version to one implemented using the state monad. Then, when the computation is replayed, repeated calls to ref may return the same reference cell, allowing the state to escape, thereby allowing di erent R j to be distinguished. What this means is, because thermometer continuations require rewriting uses of mutable state, and not only uses of shift and reset, they are not a macro-expression. So, Thielecke's theorem does not apply.

RELATED WORK

Our work is most heavily based on Filinski's work expressing monads using delimited control. 17 We have also discussed theoretical results regarding the inter-expressibility of exceptions and continuations in Section 8. Other work on implementing continuations using exceptions relate the two from a runtime-engineering perspective and from a typing perspective.

Continuations from stack inspection. Oleg Kiselyov's delimcc library18 provides an implementation of delimited control for OCaml, based on the insight that the stack-unwinding facilities used to implement exceptions are also useful in implementing delimited control. Unlike our approach, delimcc works by tightly integrating with the OCaml runtime, exposing low-level details of its virtual machine to user code. Its implementation relies on copying portions of the stack into a data structure, repurposing functionality used for recovering from stack over ows. It hence would not work for e.g.: many implementations of Java, which recover from stack over ows by simply deleting activation records. On the other hand, its low-level implementation makes it e cient and lets it persist delimited continuations to disk. A similar insight is used by Pettyjohn et al19 to implement continuations using a global program transformation.

Replay-based web programming. WASH Server Pages20 is a web framework that uses replay to persist state across multiple requests. In WASH, a single function can both display an HTML form to the end user, and take an action based on the user's response: the function is run twice, with the user's response stored in a replay log. MFlow ,21 another web framework, works along similar principles. Our work shows how the replay logs used by these frameworks are equivalent to serializing a continuation, revealing the connection between MFlow/WASH and continuation-based web frameworks.

Typing power of exceptions vs. continuations. Lillibridge 22 shows that exceptions introduce a typing loophole that can be used to implement unbounded loops in otherwise strongly-normalizing languages, while continuations cannot do this, giving the slogan "Exceptions are strictly more powerful than call/cc." As noted by other authors,23 this argument only concerns the typing of exceptions rather than their execution semantics, and is inapplicable in languages that already have recursion.

CONCLUSION

Filinski's original construction of monadic re ection from delimited continuations, and delimited continuations from normal continuations plus state, provided a new way to program for the small fraction of languages which support rst-class continuations. With our demonstration that exceptions and state are su cient, this capability is extended to a large number of popular languages, including 9 of the TIOBE 1024 (all but C). While languages like Haskell with syntactic support for monads may not bene t from this construction, bringing advanced monadic e ects to more languages paves the way for ideas spawned in the functional programming community to in uence a broader population.

In fact, the roots of this paper came from an attempt to make one of the bene ts of monads more accessible. We built a framework for Java where a user could write something that looks like a normal interpreter for a language, but, executed di erently, it would become a ow-graph generator, a static analyzer, a compiler, etc. Darais25 showed that this could be done by writing an interpreter in the monadic style (concrete interpreters run programs directly; abstract interpreters run them nondeterministically). We discovered this concurrently with Darais, and then discovered replaybased nondeterminism so that Java programmers could write normal, non-monadic programs.

Despite the apparent ine ciency of thermometer continuations, the surprisingly good performance results of Section 7, combined with the oft-unused speed of modern machines, provide hope that the ideas of this paper can nd their way into practical applications. Indeed, Filinski's construction is actually known as a way to make programs faster. 26 Overall, we view nding a way to bring delimited control into mainstream languages as a signi cant achievement. We hope to see a ourishing of work with advanced e ects now that they can be used by more programmers.

Working code for all examples and benchmarks, as well as the CPS-bind and direct-return optimizations, is available from https://github.com/jkoppel/thermometer-continuations .

A IS THIS POSSIBLY CORRECT?

In this section, we provide a correctness proof of our construction in the simplest possible case: the replay-based implementation of non-deterministic computations with a two-argument choose x y e ect, in a simplistic toy language.

We consider programs with the following grammar. Notice that, for simplicity, we only allow our choice operator choose x to take variables as parameters, rather than arbitrary expressions.

t, u ::= terms | x, , z... variables | n ∈ N number constant | S t successor | let x = t in u let-binding | choose x choice
Remarkably, this machine never duplicates a continuation K; it can be implemented in language lacking this runtime capability. It is also interesting to compare how K and (P, F ) grow over time: K grows at each reduction, including of non-e ectful operations, while (P, F ) only changes at the call sites of choose n 1 n 2 and on halt -it only records the e ects in the computation tree. Tracking the value of K in user code would require inserting code around all operations27 , while (P, F ) can be tracked by clever implementations of the e ectful operators only.

Correctness of the history machine means that the second reduction semantics produces the same results as the rst one. One way to do this is to convince ourselves that the reductions of both machine embed in the following combined machines, where machine continuations K are annotated with a past P -including in the soup -and the machine also holds a future F :

(S t, K P , F , s, R) u → (t, (S K) P , F , s, R) u (n, (S K) P , F , s, R) u → (n + 1, K P , F , s, R) u (let x = t in t , K P , F , s, R) u → (t, (let x = in (t , K)) P , F , s, R) u (n, (let x = in (t , K)) P , F , s, R) u → (t [x ← n], K P , F , s, R) u (choose n 1 n 2 , K P , ∅, s, R) u → (n 1 , K P .1 , ∅, (n 2 , K P .2 ).s, R) u (n, halt P , ∅, (n , K P ).s, R) u → (n , K P , ∅, s, n.R) u (choose n 1 n 2 , K P , i.F , s, R) u → (n i , K P .i , F , s, R) u
For the easy rules (the rst four), if you erase histories you get valid continuation reductions, and if you erase soups you get valid history reductions -both machines coincide exactly on those reductions. The subtleties come from the hard rules, the second pack.

The rst hard rule is a valid continuation rule, and corresponds to the composition of two valid history rules. The second hard rule is a valid continuation rule, but in the history system it corresponds to the halt rule followed by an arbitrarily-long series of replay computations, which use the third hard rule (they are the only part of the simulation argument to use this rule). More precisely, we claim that the following sequence, after erasing into history machines, is a valid sequence of history reductions. 28 (n, halt P , ∅, (n , K

P ).s, R) u → (u, halt ∅ , P , s, n.R) u → * (n , K P , ∅, s, n.R) u
We now establish the technical results that show that this is indeed a valid history reduction sequence. The rst step looks like the third history reduction rule, except that a seemingly-arbitrary past P taken from the soup replaces P+1. We show that a P at the head of the soup must in fact be equal to P+1 -this is part of the Timeline Invariant Lemma A.1. The second step corresponds to replaying the computation that reached (n , K P ); its correctness is established by the Replay Theorem A.5.

L A.1 (T ).

For any combined machine obtained by reducing a valid initial con guration, the rst history at the top of the soup is the successor of the continuation's history, and the second history in the soup is the successor of the rst, etc. Formally:

or require here. Indeed, you can use macros to rede ne each operation of the language to rst record the operation in a log and then perform the operation; a continuation can then be represented as the relevant fragment of the log. This technique has been mentioned to us, in private communication, by Matthias Felleisen. 28 As given, it is not a sequence of reductions for the combined machine, but this could be obtained by changing the reduction rule for (n, halt P , ∅, (n , K P ).s, R) u to reduce to (u, halt ∅ , P , s, n.R) u instead of (n , K P , ∅, s, n.R) u . The fact that these two reduction choices are equivalent, from the point of view of the nal result, is precisely the Replay Theorem A. Capturing the Future by Replaying the Past 10:31

(1) For any con guration of the form (n, K P , ∅, (n , K P ).s, R) u , we have P = P+1.

(2) For any (sub)-soup of the form (n, K P ).(n , K P ).s occurring inside a con guration, we have P = P+1.

P

. All combined reductions preserve this invariant. The only non-trivial case being the rst hard rule, which adds a new history-annotated continuation to the soup.

(choose

n 1 n 2 , K P , ∅, s, R) u → (n 1 , K P .1 , ∅, (n 2 , K P .2 ).s, R) u
By assumption on the input con guration, the rst history on top of the soup s (if any) is P+1. This is also equal to (P .2)+1, so the new soup (n 2 , K P .2 ).s respects the invariant (2). Furthermore, the new history on the top of the soup, P .2, is indeed the successor of the history of the new annotated continuation, K P .1 , so the invariant (1) is also respected.

De nition A.2 (Pure reduction). We de ne the pure reductions as the subset of combined machine reductions that do not depend on the soup s or result R -the four easy rules, plus the last hard rule. We write (→ pure ) for pure reductions.

Notice that pure reduction sequences are all of the following form 29 (t 1 , (K 1 ) P , P .F , s, R) u → * pure (t 2 , (K 2 ) P .P , F , s, R) u

The soup and results are unchanged, and a fragment of history moves from the future to the past.

L A.3 (M

). Pure reductions are insensitive to changes of soups and results, and they are preserved by appending to the future. If (t 1 , K 1P , (P .F ), s, R) u → * pure (t 2 , K 2(P .P ) , F , s, R) u , then for any s , R and F we also have (t 1 , K 1P , (P .F .F ), s , R ) u → * pure (t 2 , K 2(P .P ) , (F .F ), s , R ) u . De nition A.4 (Replay con guration replay(c)). We de ne the replay con guration of an arbitrary con guration by replay(t, K P , F , s, R) u def = (u, halt ∅ , (P .F ), s, R) u .

T

A.5 (R ). If a combined con guration c is reachable from an initial con guration, then its replay con guration replay(c) reduces back to c using only pure steps:

((u, halt ∅ , ∅, ∅, ∅) u → * c) =⇒ (replay(c) → * pure c)) P .
The proof proceeds by induction on the combined reduction sequence from an initial state (u, halt ∅ , ∅, ∅, ∅) u to an arbitrary con guration (n, K P , F , s, R) u , building a corresponding sequence of combined reduction steps starting from its replay con guration (u, halt ∅ , (P .F ), s, R) u .

Either the reduction sequence is empty, in which case the result is immediate, or we consider its last reduction step c → c. By induction hypothesis, c is purely reachable from replay(c ), and we have to transport this reduction repla (c ) → * pure c into a reduction replay(c) → * pure c. We reason by case analysis on the reduction c → c. If it is a pure reduction, we have replay(c ) = replay(c) and can conclude by replaying the reduction from c unchanged. The delicate cases correspond to the two impure reductions.

First impure reduction.

(choose n 1 n 2 , K P , ∅, s, R) u → (n 1 , K P .1 , ∅, (n 2 , K P .2 ).s, R) u
Let us de ne s as (n 2 , K P .2 ).s. We want to show that (n 1 , K P .1 , ∅, s , R) u is purely reachable from (u, halt ∅ , P .1, s , R) u . By induction hypothesis, we know that (u, halt ∅ , P, s, R) u purely reaches (choose n 1 n 2 , K P , ∅, s, R) u . By monotonicity (Lemma A.3), we know that (u, halt ∅ , P .1, s , R) u purely reaches (choose n 1 n 2 , K P , 1.∅, s , R) u , and we conclude with a pure reduction step. This proof can be represented better in diagrammatic form: (n 1 , halt P , ∅, (n 2 , K

(u, halt ∅ , ∅, ∅, ∅) u (choose n 1 n 2 , K P , ∅, s, R) u (n 1 , K P .1 , ∅, s , R) u (u, halt ∅ , P, s, R) u (choose n 1 n 2 , K P , ∅, s, R) u (u, halt ∅ , P .1.∅, s , R) u (choose n 1 n 2 , K P , 1.∅, s , R) u (n 1 , K P .1 , ∅, s ,
P ).s, R) u → (n 2 , K P , ∅, s, n 1 .R) u
By the Timeline Invariant (Lemma A.1), we know that P = P+1. Let us pose R def = n 1 .R. In the reduction sequence from the initial con guration to (n 1 , halt P , ∅, (n 2 , K P ).s, R) u , let us consider the rst reduction that pushed the paused computation (n 2 , K P ) onto the soup s. This reduction can only be of the form (choose n 1 n 2 , K P 0 , ∅, s, R) u → (n 1 , K P 0 .1 , ∅, (n 2 , K P 0 .2 ).s, R) u for a certain history P 0 such that P 0 .2 = P+1.

The proof follows from these de nitions by the following diagram:

(u, halt ∅ , ∅, ∅, ∅) u (choose n 1 n 2 , K P 0 , ∅, s, R) u (n 2 , K P +1 , ∅, s, n 1 .R) u (u, halt ∅ , P 0 , s, R) u (choose n 1 n 2 , K P 0 , ∅, s, R)

B PARSING BENCHMARKS

To get a sense of the performance cost of thermometer continuations in realistic e ectful programs, we wrote direct-style versions of monadic parsing programs. There are three benchmarks, all implemented in SML: INTPARSE GLOB, INTPARSE LOCAL, and MONADIC ARITH PARSE. They use two di erent monads. Depending on the monad, we gave three to six implementations of each benchmark. Each Indirect implementation implements the program pure-functionally, without monadic e ects. The ThermoCont and Filinski implementations use monadic re ection, implemented via thermometer continuations and Filinski's construction, respectively (using SML's native call/cc support). For the nondeterminism and failure monads, our optimizations apply, given in Opt. ThermoCont.

Capturing the Future by Replaying the Past 10:33 Numbers were collected using SML/NJ v110.80. 30 We could not port our implementation to MLTON,31 the whole-program optimizing compiler for SML, because it lacks Unsafe.cast. The experiments in this section only were conducted on a 2015 MacBook Pro with a 2.8 GHz Intel Core i7 processor. All times shown are the average of 5 trials, except for MONADIC ARITH PARSE, as discussed below.

The twin benchmarks INTPARSE GLOB and INTPARSE LOCAL both take a list of numbers as strings, parse each one, and return their sum. They both use a monadic failure e ect (like Haskell's Maybe monad), and di er only in their treatment of strings which are not valid numbers: INTPARSE GLOB returns failure for the entire computation, while INTPARSE LOCAL will recover from failure and ignore any malformed entry. Table 2 gives the results for INTPARSE GLOB. For each input size n, we constructed both a list of n valid integers, as well as one which contains an invalid string halfway through. Table 3 gives the results for INTPARSE LOCAL. For each n, we constructed lists of n strings where every 1/100th, 1/10th, or 1/2nd string was not a valid int. For INTPARSE GLOB, unoptimized thermometer continuations wins, as it avoids Filinski's cost of call/cc, the optimized version's reliance on closures, as well as the indirect approach's cost of wrapping and unwrapping results in an option type. For INTPARSE LOCAL, unoptimized thermometer continuations lost out to the indirect implementation, as thermometer continuations here devolve into raising an exception for bad input, which is slower than wrapping a value in an option type.

Finally, benchmark MONADIC ARITH PARSE is a monadic parser in the style of Hutton and Meijer. 32 These programs input an arithmetic expression, and return the list of results of evaluating any pre x of the string which is itself a valid expression. The Filinski and ThermoCont implementations closely follow Hutton and Meijer, executing in a "parser" monad with both mutable state and nondeterminism (equivalent to Haskell's StateT List monad). The indirect implementation inlines the monad de nition, passing around a list of (remaining input, parse result) pairs. We did not provide an implementation with optimized thermometer continations, as we have not yet found how to make our optimizations work with the state monad. Note that all three implementations use the same algorithm, which, although the code imeplementing it is beautiful (at least for the monadic versions), is exponentially slower than the standard LL/LR parsing algorithms.

Table 4 reports the average running time of each implementation on 30 random arithmetic expressions with a xed number of leaves. There was very high variance in the running time of di erent inputs, based on the ambiguity of pre xes of the expression. For inputs with 40 leaves, the fastest input took under 25ms for all implementations, while the slowest took approximately 25 minutes on the indirect implementation, 5 hours and 43 minutes for Filinski's construction, and 9 hours 50 minutes for thermometer continuations.

Overall, these benchmarks show that the optimizations of Section 6 can provide a substantial speedup, and there are many computations for which thermometer continuations do not pose a prohibitive cost. Thermometer continuations are surprisingly competitive with Filinski's construction, even though SML/NJ is known for its e cient call/cc, and yet thermometer continuations can be used in far more programming languages. Gergö Barany helped benchmark our Prolog implementation; he suggested using GNU Prolog instead of SWI Prolog, which was giving much slower results.

This material is based upon worked supported by the National Science Foundation under Grant No. 1122374. Any opinions, ndings, and conclusions or recommendations expressed in this material are those of the authors and do not necessarily re ect the views of the National Science Foundation.

  structure ListMonad : MONAD = struct type α m = α list fun return x = [x] fun bind [] f = [] | bind (x :: xs) f = f x @ bind xs f end;

  val firstTime = ref false ( * choose2 : α * α → α * ) fun choose2 (x1,x2) = if !firstTime then x1 else x2 ( * withNondeterminism2 : (unit → α ) → α list * ) fun withNondeterminism2 f = [(firstTime := true ; f ()), (firstTime := false; f ())] -withNondeterminism2 (fn () ⇒ 3 * choose2 (5,6)) ( * val it =

Fig. 1 .

 1 Fig. 1. Several points in the execution of the replay-based nondeterminism algorithm.

  val past : idx list ref = ref [] val future : idx list ref = ref [] ( * auxiliary stack functions * ) fun push stack x = (stack := x :: !stack) fun pop stack = case !stack of [] ⇒ NONE | x :: xs ⇒ (stack := xs; SOME x)

  val nest : (idx list * idx list) list ref = ref [] result = loop f [] in ( * then restore them after the run * ) case pop nest of NONE ⇒ raise Impossible | SOME (p, f) ⇒ (past := p; future := f; result)) end and fun loop f acc = ( * by the way, let's use a tail-recursive function * ) let val acc = ([f ()] handle Empty ⇒ []) @ acc val next _ future = List.rev (next _ path (!past))

Fig. 2 .

 2 Fig. 2. To implement choose: first capture the continuation, and then use the list monad's bind operator to evaluate it multiple times.

Fig. 3 .

 3 Fig. 3. Graphical depiction of the action of the shift operator.

exception

  Done of int fun reset f = (cur _ expr := f; state := NONE; (f () handle (Done x) ⇒ x))

  Fig. 4. A thermometer continuation before being invoked

Fig. 5 .

 5 Fig. 5. Graphical depiction of running a thermometer continuation

Figure 4

 4 Figure4depicts a thermometer continuation, while Figure5animates the execution of a thermometer continuation, showing how each value in the future matches with a shift, until the function being evaluated has the desired continuation. The left side of Figure5resembles a thermometer sticking out of the function, which inspired the name "thermometer continuations. "

  7 signature UNIVERSAL = sig type u; val to _ u : α → u; val from _ u : u → α ; end; structure Universal : UNIVERSAL = struct datatype u = U; val to _ u = Unsafe.cast; val from _ u = Unsafe.cast; end;

  fun run _ with _ future f f _ future = (push nest (!cur _ expr, !past, !future); past := []; future := f _ future; cur _ expr := f; let val result = (f () handle (Done x) ⇒ x) in case pop nest of NONE ⇒ raise Impossible | SOME (prev _ expr, prev _ past, prev _ future) ⇒ (cur _ expr := prev _ expr; past := prev _ past; future := prev _ future; result) end)

Figure 2

 2 Figure2showed how nondeterminism can be implemented by binding a value to the (delimited) continuation. The de nition of reflect is a straightforward generalization of this.fun reflect m = C.shift (fn k ⇒ M.bind m k)

  reify (fn () ⇒ let val x = choose [2,3,4] * choose [5,7] in if x ≥ 20 then x else fail () end) ( * val it =

functor

  StateMonad (type state) : MONAD = struct type α m = state → α * state fun return x = fn s ⇒ (x, s) fun bind m f = fn s ⇒ let val (x, s') = m s in f x s' end end; structure S = Represent(StateMonad(type state = int)) fun tick () = S.reflect (fn s ⇒ ((), s+1)) fun get () = S.reflect (fn s ⇒ (s, s)) fun put n = S.reflect (fn _ ⇒ ((), n)) -#1 (S.reify (fn () ⇒ (put 5; tick (it = 12 : int * )

  5. Proc. ACM Program. Lang., Vol. 2, No. ICFP, Article 10. Publication date: September 2018.

  fun choose [] = raise Empty | choose xs = case pop future of NONE ⇒ ( * no future: start a new index; push it into the past * )

	let val i = start _ idx xs in
	push past i;
	get xs i
	end
	| SOME i ⇒ (push past i;
	get xs i)

  datatype frame = RETURN of Universal.u | ENTER Then, the desired computation is expressed simply as [ENTER], and can be evaluated with value x using the future [ENTER, RETURN x]. The values past and future are stacks of this frame type. val past : frame list ref = ref [] val future : frame list ref = ref []Nested reset's. This is handled completely analogously to Section 2.4: each call to reset will push the previous state to a nesting stack, and restore it afterwards.

type reset _ state = (unit → ans) * state list * state list val nest : reset _ state list ref = ref []

Table 1 .

 1 Benchmark NQUEENS

  halt ∅ , P 0 .2.∅, s, R ) u (choose n 1 n 2 , K P 0 , 2.∅, s, R ) u (n 2 , K P 0 .2 , ∅, s, R ) u * *

	ind. hyp. ind. hyp.
	*
	pure
	monotonicity
	*
	pure	pure
	replay

u (u,

Table 2 .

 2 Benchmark INTPARSE GLOBBad input? 10,000 50,000 100,000 500,000 1,000,000 5,000,000

	Indirect	Y 0.001s 0.004s N 0.002s 0.014s	0.026s 0.051s	0.159s 0.213s	0.260s 0.371s 1m54.072s 36.260s
	Filinski	Y 0.004s 0.018s N 0.004s 0.020s	0.012s 0.014s	0.172s 0.221s	0.258s 0.360s 1m26.603s 28.719s
	Therm.	Y 0.003s 0.008s N 0.003s 0.011s	0.024s 0.029s	0.167s 0.223s	0.260s 0.367s 1m20.841s 27.461s
	Therm. Opt	Y 0.000s 0.013s N 0.002s 0.015s	0.015s 0.024s	0.197s 0.247s	0.293s 0.364s 1m23.433s 27.298s

Table 3 .

 3 Benchmark INTPARSE LOCAL% bad input 10,000 50,000 100,000 500,000 1,000,000 5,000,000

		1% 0.002s 0.011s	0.060s	0.232s	0.404s 2m04.316s
	Indirect	10% 0.002s 0.002s	0.053s	0.221s	0.375s 1m40.251s
		50% 0.000s 0.001s	0.014s	0.176s	0.257s	15.515s
		1% 0.001s 0.048s	0.053s	0.264s	0.456s 3m19.265s
	Filinski	10% 0.004s 0.043s	0.048s	0.234s	0.420s 3m22.245s
		50% 0.004s 0.008s	0.050s	0.182s	0.301s	23.632s
		1% 0.002s 0.045s	0.064s	0.344s	0.470s 4m28.700s
	Therm.	10% 0.003s 0.028s	0.064s	0.266s	0.445s 3m26.809s
		50% 0.001s 0.023s	0.067s	0.214s	0.353s	22.750s
		1% 0.002s 0.030s	0.059s	0.227s	0.403s 3m01.981s
	Therm. Opt.	10% 0.002s 0.030s	0.058s	0.218s	0.386s 2m37.321s
		50% 0.002s 0.023s	0.055s	0.183s	0.289s	27.546s

Table 4 .

 4 Benchmark MONADIC ARITH PARSE

	10	20	30	40
	Indirect 0.011s 0.163s 1.908s 2m39.860s
	Filinski 0.116s 1.638s 19.035s 25m18.794s
	Therm. 0.184s 2.540s 30.229s 39m58.183s

[START_REF] Filinski | Representing Monads[END_REF]

TIOBE Software BV (2017) Proc. ACM Program. Lang., Vol. 2, No. ICFP, Article 10. Publication date: September 2018.

Proc. ACM Program. Lang., Vol. 2, No. ICFP, Article 10. Publication date: September 2018.

[START_REF] Felleisen | The Theory and Practice of First-Class Prompts[END_REF] Proc. ACM Program. Lang., Vol. 2, No. ICFP, Article 10. Publication date: September 2018.

[START_REF] Kent Dyvbig | A Monadic Framework for Delimited Continuations[END_REF]

[START_REF] Asai | Introduction to Programming with Shift and Reset[END_REF] Proc. ACM Program. Lang., Vol. 2, No. ICFP, Article 10. Publication date: September 2018.

See Section 4.4 for a discussion of our use of unsafe casts. Proc. ACM Program. Lang., Vol. 2, No. ICFP, Article 10. Publication date: September 2018.

[START_REF] Filinski | Representing Monads[END_REF]

[START_REF] Filinski | Representing Layered Monads[END_REF]

See several implementations at http://mlton.org/UniversalType Proc. ACM Program. Lang., Vol. 2, No. ICFP, Article 10. Publication date: September 2018.

[START_REF] Kiselyov | Delimited Control in OCaml, Abstractly and Concretely: System Description[END_REF]

[START_REF] Kiselyov | E directly in OCaml[END_REF]

This runtime o ers very e cient one-shot continuations, but copying continuations is unsafe and may be less optimized. Proc. ACM Program. Lang., Vol. 2, No. ICFP, Article 10. Publication date: September 2018.

[START_REF] Felleisen | On the Expressive Power of Programming Languages[END_REF]

[START_REF] Thielecke | Contrasting Exceptions and Continuations[END_REF] Proc. ACM Program. Lang., Vol. 2, No. ICFP, Article 10. Publication date: September 2018.

[START_REF] Filinski | Representing Monads[END_REF]

[START_REF] Kiselyov | Delimited Control in OCaml, Abstractly and Concretely: System Description[END_REF]

[START_REF] Pettyjohn | Continuations from Generalized Stack Inspection[END_REF]

[START_REF] Thiemann | WASH Server Pages[END_REF]

[START_REF] Gomez | MFlow, a Continuation-Based Web Framework Without Continuations[END_REF]

[START_REF] Lillibridge | Unchecked Exceptions Can Be Strictly More Powerful Than Call/CC[END_REF] Proc. ACM Program. Lang., Vol. 2, No. ICFP, Article 10. Publication date: September 2018.

[START_REF] Thielecke | Contrasting Exceptions and Continuations[END_REF]

TIOBE Software BV (2017)

[START_REF] Darais | Abstracting De nitional Interpreters (Functional Pearl)[END_REF]

[START_REF] Hinze | Kan Extensions for Program Optimisation or: Art and Dan Explain an Old Trick[END_REF] Proc. ACM Program. Lang., Vol. 2, No. ICFP, Article 10. Publication date: September 2018.

This typically requires compiler support. It can also be done with rich enough a macro system -which we don't assume

We write P .F and P .P and F .F for the concatenation of histories. Proc. ACM Program. Lang., Vol. 2, No. ICFP, Article 10. Publication date: September 2018.

[START_REF] Andrew | Standard ML of New Jersey[END_REF]

[START_REF] Weeks | Whole-Program Compilation in MLton[END_REF]

[START_REF] Hutton | Monadic Parsing in Haskell[END_REF] Proc. ACM Program. Lang., Vol. 2, No. ICFP, Article 10. Publication date: September 2018.

ACKNOWLEDGMENTS

We warmly thank Ben Lippmeier, Edward Z. Yang, Adam Chlipala, and the anonymous reviewers for the excellent feedback they provided on this article.

Capturing the Future by Replaying the Past 10:29

To make the execution semantics of non-deterministic choice precise, we de ne a notion of abstract machine, the continuation machines. Continuation machines are quadruples (t, K, s, R) of a term, a current machine continuation K, a soup of threads s that are waiting to execute next, and a result R, which is a list of integers. Machine continuations are given by the following very simple grammar, and thread soups s are lists of pairs (t, K) of a term and a continuation.

We can now de ne a reduction relation (t, K, s , R) → (t , K , s , R ) to formally de ne the execution of those non-deterministic programs. If (t, halt, ∅, ∅) eventually reduces to (n, halt, ∅, R) for n ∈ N, then the meaning of the program is the result n.R.

The two last rules give the semantics of parallelism: choose n 1 n 2 returns n 1 , but adds the thread (n 2 , K) to the soup, to be executed later, and the halt rule adds a number to the result set and restarts the next thread in the soup. Note that the rule for choose n 1 n 2 duplicates the continuation K, pushing a copy of it on the thread soup: this machine relies on a runtime that can duplicate continuations.

Our correctness argument comes from de ning an alternative notion of abstract machines for these programs, the history machines, that model our replay-based technique. History machines are of the form (t, K, P, F , R) u , replacing the soup s with a past P and a future F which are just ordered lists of choices i -numbers in {1, 2}. Finally, u is the initial computation that gets replayed; it is constant over the whole execution. We also de ne an operation P+1 corresponding to the next _ path function of Section 2.3, which gets stuck on an empty path: ∅+1 is just ∅+1.

When we reach an instruction choose n 1 n 2 , the machine picks the value n i (for i ∈ {1, 2}) determined by its future F . When it nishes computing a value n with past history P, it restarts the whole computation u with future P+1, exactly like our implementation. The semantics of a term t for these machines is given by the result R such that (t, halt, ∅, ∅, ∅) t reduces to (t, halt, ∅, ∅+1, R) t .