.
While a powerful semantic construct, there is no secret that continuations are, for good reason, thoroughly reviled . In short, give us the ability to store the state of a computation and return to it at a later point in the execution of our program. This form of non-local return (or jump anywhere ) can be compared to goto
statements of old or try
/catch
semantics commonly seen today. Both of these language features, in fact, can be implemented in terms of continuations, and in section 5.8.3 of The Ruby Programming Language , the authors construct a BASIC inspired goto
function. Today, when we require 'continuation'
, the interpreter will kindly inform us that 'warning: callcc is obsolete; use Fiber instead'.
With continuations framed in this way, it should go without saying that, under no circumstances whatsoever, should these curios be used in anything with any degree of seriousness. Why, then, should should we even bother? Sometimes, the simple fact that we should not is enough to make it worthwhile.
Because callcc
is obsolete, the continuation library is not included by default in Ruby. Each example contained herein requires require 'continuation'
as a prelude before being able to call Kernel#callcc . The callcc
function takes a block, which is passed an instance of a continuation object , and ultimately returns whatever the block returns. These first examples will only run inside IRB, since they otherwise cause an infinite unwinding of the stack (e.g. every time the continuation is call
ed, execution moves up the program, only to encounter the same call
ad infinitum).
Here, we create a global variable in which we will store our continuation. As this example shows, we can call the continuation repeatedly, and it will return whatever we pass into into the method. The final key point in the demonstration is the way in which subsequent computation is safely discarded. In this case, the + 1
call does not result in the usual TypeError: no implicit conversion of Integer into String
.
As an aside, it is worth noting that it is possible to make IRB act more like the interpreter proper. Two ways to prevent it from taking control of the stack and allowing these examples to run without looping indefinitely are:
puts "the call/cc returned #{callcc { |c| $cont = c; 'a' }}"; $cont.call
)Kernel#callcc
and the Continuation#call
inside a begin
..end
blockOur next example demonstrates that the stack is shared between continuations. We can call either one, and receive successive numbers.
In this next example, we will implement a depth first traversal for trees represented as nested arrays of values (inspired by the homoiconicity of Lisp). Rather than using a traditional recursive method, we make use of continuations to pause the calculation at appropriate points, only to restart it at any point in the future. Tree#dft
processes the entire tree at once, storing intermediate calculations (cf. thunks ) in the saved
array, which are then processed after the current branch is completed.
And the output is identical to what you would expect with the recursive solution.
The most interesting aspects of this implementation, however, is not simply that it produces the expected result. The first thing to notice, as Graham points out, is the lack of any explicit iteration or recursion. Control flow is entirely handled through the partial calculations we store as continuations and restart until we have completed the traversal. "Search with continuations represents a novel way of thinking about programs: put the right code in the stack, and get the result by repeatedly returning up through it."
The second novel feature of this implementation is that we are able to take control of it using the Tree#dft_node
and Tree#restart
methods. Once we have an instance of a tree, we can take off as many nodes as we want. It is possible, in fact, to turn this implementation into a lazy enumerator with very little effort. This next figure displays the output when the flow is explicitly handled by the caller.
This article has glossed over a very key point: Lisp does not actually have continuations, and all these examples are taken from Scheme as a way to frame an implementation of continuations in Lisp using macros. This is accomplished using macros, which, in Lisp, are statically interpolated, making it possible to fall back on lexical scope and variable shadowing as the primary mechanisms for implementing semantics similar to continuations. Since metaprogramming in Ruby is dynamically performed at runtime, we do not have the same facilities at our disposal. We will have to, instead, take a different approach to the problem and, likely, abuse some language features to accomplish the same result.
29 May 2017