Implementing python style generators with delimited continuations
12 Sep 2010
Jake’s article on implementing LWT-compatible fibers with Oleg’s delimited continuation library got me thinking about one of my favorite Python features: generators.
Delimited continuations
Delimited continuations have a very simple API (note: I’m going to
consider only the high level API provided by delimcc here consistent
with the abstract view of delimited continuations. Jake describes the
low-level API in
his article).
The core API is:
val new_prompt   : unit -> 'a prompt
val push_prompt  : 'a prompt -> (unit -> 'a) -> 'a
val shift0       : 'a prompt -> (('b -> 'a) -> 'a) -> 'bnew_prompt simply creates the “handle”, push_prompt (traditionally
called reset) sets the limit of the continuation (how far up the
stack we will go), shift0 captures the continuation and gives it to
the passed function. Crucially, the continuation captured by shift0
is delimited by the outer push_prompt. The “region” of the
continuation delimited by push_prompt and shift0 consists of the
stack frames between the two (this also means you cannot call shift0
anywhere outside the scope of push_prompt).
An example should clarify. Let’s first do something illegal.
# open Delimcc;;
# let p = new_prompt ();;
value p : Delimcc.prompt '_a = <abstr>
# push_prompt p identity;;
- : unit = ()
# shift0 p (fun k -> k ());;
Exception: Failure "No prompt was set".Here, we tried to shift0 outside of the push_prompt. Nope, can’t
do that.
# push_prompt p (fun () -> shift0 p (fun k -> k 123));;
- : int = 123Here, the inner function (passed to shift0) was given the captured
continuation k (as delimited by push_prompt), calling it with the
argument 123 which was subsequently returned.
The neat thing about delimited continuations is that they capture the
state entire delimited stack. That is, calling k ARG from within
shift0 is equivalent to replacing the shift0 statement with ARG,
returning the result of push_prompt. Furthermore, it’s a first class
continuation, so we may use it multiple times. To wit:
# push_prompt p (fun () -> 10 * shift0 p (fun k -> k 1 + k 3));;
- : int = 40Here, k is equivalent to the function ( * ) 10. The argument &
return types needn’t be uniform:
# push_prompt p (fun () -> string_of_int (shift0 p (fun k ->  "hey " ^ k 123)));;
- : string = "hey 123"Leaking continuations
What makes delimited continuations yet more interesting is that the
continuations themselves can escape the scope of shift0, and be
restarted from outside.
Because of the type signatures of shift0, we need to unify the
types, so let’s first define an ADT to do so:
# type t = Done | More of (unit -> t);;Note that we can’t just use option here, it seems, because we need a
recursive type.
# let count = ref 0;;
# let More k = push_prompt p (fun () -> 
  while true do 
    count := !count + 1; 
    shift0 p (fun k -> More k)
  done; 
  Done);;Here we are using our unified type trick to return the continuation
itself (note how push_prompt and shift have the same return
type). Let’s play!
# !count;;
- : int = 1
# k ();;
- : t = More <fun>
# k ();;
- : t = More <fun>
# !count;;
- : int = 3Generators
We now have a pretty good idea of what’s needed to implement generators with an API similar to:
let producer () =
  let state = ... in
  gen begin fun yield ->
    yield state; ...; yield state; ...; yield state
  endSo we need a function gen that takes a “yielder” (in Python
terminology) that needs to capture the continuation, communicate the
value and resume execution when the next value is asked for by the
consumer:
let consumer () = 
  let g = producer () in
  let v0 = g () in
  ...
  let vN = g ()It turns out that this is a very natural fit, and we can implement full duplex generators in just a few lines, using the ideas developed above. Here is the full source code:
type ('a, 'b) t = Done | More of 'a * ('b -> ('a, 'b) t)
let gen f =
  (*
   * Note: the first value to yield gets thrown away as the generator
   * has not yet started.
   *)
  let start _ =
    let p = Delimcc.new_prompt () in
    Delimcc.push_prompt p begin fun () ->
      f (fun x -> Delimcc.shift0 p (fun k -> More (x, k))); Done
    end in
  let next = ref start in
  fun rv ->
    match !next rv with
      | More (x, k) -> next := k; Some x
      | Done        -> NoneAnd sample use:
let rec take_all t count =
  match t count with
    | Some i -> printf "took: %d\n" i; take_all t (count + 1)
    | None   -> ()
let () =
  let take = gen begin fun yield ->
    for i = 0 to 10 do
      let rv = yield i in
      printf "got: %d\n" rv
    done
  end in
  take_all take 100The only additional work we have to do is to type it for bi-directional communication, and to store the next continuation in a ref cell for the next invocation. Given that the generators developed here are full duplex, we can implement co-routines as outlined in PEP 342.
You can also get it as a gist here. I
also highly recommend
Jake’s article
which explores delimcc from a lower level of abstraction.