Implementing yield

First, let's see what yield is by looking at some usage examples.

foo = with-yield(λ(yield){
  yield(1);
  yield(2);
  yield(3);
  "DONE";
});

println(foo());  # prints 1
println(foo());  # prints 2
println(foo());  # prints 3
println(foo());  # prints DONE

When called, yield stops execution of the function and returns a value. Pretty much like return so far. But when you call the function again, it resumes from where the last yield left off, as you can see in the above example. It prints 1, 2, 3, DONE.

A more real-world usage:

fib = with-yield(λ(yield){
  let loop (a = 1, b = 1) {
    yield(b);
    loop(b, a + b);
  };
});

## print the first 50 fibonacci numbers
let loop (i = 0) {
  if i < 50 {
    println(fib());
    loop(i + 1);
  };
};

The fib function contains an infinite loop. There is no termination condition. But yield interrupts that loop and returns the current number. Calling the function again resumes from where it left off, so to print the first 50 numbers then we just need to call the function 50 times.

This program will also run much faster than the duble-recursive version of fib, because it keeps track of the previous two values internally and just returns the next one.

A failed implementation attempt

I wouldn't waste your time with a failed attempt unless there were valuable lessons to learn from it, so please bear with me.

To implement yield it seems we need to save the initial continuation of the function. We need that so we can exit early, like we do for return. However, before actually returning from a yield we also have to save yield's own continuation, in order to be able to return later to that point inside the function. That's what it seems, at least. We can attempt an implementation like this:

with-yield = λ(func) {
  ## with-yield returns a function of no arguments
  λ() {
    CallCC(λ(kret){        # kret is the "return" continuation
      let (yield) {

        ## define yield
        yield = λ(value) { # yield takes a value to return
          CallCC(λ(kyld){  # kyld is yield's own continuation…
            func = kyld;   # …save it for later
            kret(value);   # and return.
          });
        };

        ## finally, call the current func, passing it the yield.
        func(yield);
      };
    });
  };
};

This was my first stab at implementing yield and it all seemed to make sense. But if we try to run the first sample above for this definition of with-yield, it will be an endless loop. Try it below if you wish, but be prepared to refresh the page (I actually modified Execute in this page to use setTimeout instead of exceptions for clearing the stack, so it won't freeze your browser). If you click Run you'll briefly notice in the output that it prints 1, 2, 3, followed by an endless stream of "DONE".

The reason why it loops is subtle. But first, if you want to see something even more bizarre, edit the code above and change the last 4 println lines to this:

println(foo());
foo();

The output will be exactly the same. Again: if you ran this example you should refresh your page, otherwise it will run endlessly, draining the battery and eventually all your computer's RAM.

Problem 1: yield never changes

One problem to notice is that the continuation we save for the first yield (kyld, which becomes the next func to call, if you follow the implementation of with-yield) embeds this computation:

  yield(2);
  yield(3);
  "DONE";

But who is yield in this continuation? It's the same as the first yield, which actually returns to the first kret continuation ever saved, which is to print the result and continue from there, resulting in a loop. Fortunately there's an easy fix for that. Clearly enough, we should create the yield closure only once, since we cannot change it in the function after the first call; in order to return to the proper exit point we maintain a new return variable:

with-yield = λ(func) {
  let (return, yield) {
    yield = λ(value) {
      CallCC(λ(kyld){
        func = kyld;
        return(value);       # return is set below, on each call to the function
      });                    # it's always the next return point.
    };                       #
    λ(val) {                 #
      CallCC(λ(kret){        #
        return = kret;       # <- here
        func(val || yield);
      });
    };
  };
};

Additionally, recognizing that it doesn't make sense to pass yield each time to the function, but only the first time, this new version allows passing another value on each invocation (except for the first). That will be yield's own return value.

The following code still uses the “foo” example, but it has println(foo()) only three times:

It appears to work properly. It's a clear improvement over the first version, which looped on code like print(foo()); foo(). But what happens if you add another println(foo())? LOOP!

Problem 2: WTF?

The reason for looping this time is a little more profound. It has to do with the unlimited nature of our continuations: they encode the whole future of the computation and that happens to include returning from the first call to foo(). What happens when we return from it? — we start all over:

println(foo()); ## yield 1 <----------------- HERE -------------------+
println(foo()); ## yield 2                                            |
println(foo()); ## yield 3                                            |
println(foo()); ## we reached "DONE", so the first foo() RETURNS -->--+

Let's look at this line from with-yield:

        func(val || yield);
        #...

When func exits via an invocation of yield, it calls a continuation, so the execution doesn't reach the #... line. But by the time it finishes, for reaching the end of the function (the "DONE" line), func will have been effectively reduced to a function which returns "DONE" to the original continuation, which is to call the first print. The foo() on the second line just loops over, but all those "DONE" lines are printed by the first print line. You can verify this by running the following code:

println(foo());
println("bar");
println(foo());
println(foo());
foo();

The output will be: 1, bar, 2, 3, DONE, bar, DONE, bar, ....

So for a possible fix, we simply must set func to something else when it returns “naturally” (that is to say simply, “finishes”). We'll make it a do-nothing function which just returns "no more continuations".

        val = func(val || yield);
        func = λ() "NO MORE CONTINUATIONS";
        kret(val);

It no longer loops now, but be prepared to be confused when running the code below:

We would have hoped for 1, 2, 3, DONE, but we also get "NO MORE CONTINUATIONS" three times. To figure out what happens we can interleave some prints, like this:

print("A. "); println(foo());
print("B. "); println(foo());
print("C. "); println(foo());
print("D. "); println(foo());

## and the output is:
A. 1
B. 2
C. 3
D. DONE
B. NO MORE CONTINUATIONS
C. NO MORE CONTINUATIONS
D. NO MORE CONTINUATIONS
***Result: false

Which shows that the problem remained: a natural exit from the function still goes back to the first continuation; but because the function is now a no-op, the "B." line won't trigger a loop.

Our implementation is still useful, provided that the function never finishes and only exits via yield. Here's for the Fibonacci example:

But if we want a solid implementation (so, one that doesn't return to the first call site when the function finishes), we need another concept called “delimited continuations”.

Delimited continuations: reset and shift

We're going to implement yield in terms of two other operators, reset and shift. They provide “delimited continuations” — these are continuations that return, like an ordinary function. reset creates a frame and shift intercepts the continuation only upto that frame, instead of “the whole future of the program” like CallCC does.

Both reset and shift take a single function argument. During the execution of the reset function, a call to shift will allow us to return a value to the place where reset was called.

First, let's see how with-yield will look:

with-yield = λ(func) {
  let (yield) {
    ## yield uses shift to return a value to the innermost
    ## reset call.  before doing that, it saves its own
    ## continuation in `func` — so calling func() again will
    ## resume from where it left off.
    yield = λ(val) {
      shift(λ(k){
        func = k;  # save the next continuation
        val;       # return the currently yielded value
      });
    };
    ## from with-yield we return a function with no arguments
    ## which uses reset to call the original function, passing
    ## to it the yield (initially) or any other value
    ## on subsequent calls
    λ(val) {
      reset( λ() func(val || yield) );
    };
  }
};

Note that each call to the function is now embedded in a reset. This ensures that we don't enclose the whole continuation of the program, but only the part up to this reset. By definition it is now impossible to loop. When the function finishes naturally, the program will just continue execution instead of going back to the place of the first call.

You can run our troublesome program below and you'll see that it finally behaves as expected. It prints nothing more nor less than it should, and it doesn't loop.

Implementation of reset / shift

These operators are quite tricky to implement, although the code is short. Prepare for some headaches. I'll give you two solutions. I am not the author of this method and I can tell you I stared at the code for a long time before it clicked. I found it in this Scheme program by Oleg Kiselyov. I recommend these articles to understand more about this mind-blowing concept.

As primitives

In this page they are implemented as primitive functions. In primitives, the current continuation is explicit (we don't need CallCC), and this makes it somewhat easier to understand what's going on under the covers. Here's the complete implementation (left):

var pstack = [];

function _goto(f) {
    f(function KGOTO(r){
        var h = pstack.pop();
        h(r);
    });
}

globalEnv.def("reset", function(KRESET, th){
    pstack.push(KRESET);
    _goto(th);
});

globalEnv.def("shift", function(KSHIFT, f){
    _goto(function(KGOTO){
        f(KGOTO, function SK(k1, v){
            pstack.push(k1);
            KSHIFT(v);
        });
    });
});

It can be useful to have with-yield by the side, where we used reset and shift.

with-yield = λ(func) {
  let (yield) {
    yield = λ(val) {
      shift(λ(SK){
        func = SK;
        val;         ## return val
      });
    };
    λ(val) {
      reset( λ() func(val || yield) );
    };
  }
};

The key is that both reset and shift use _goto, which isn't a “normal” function. It has no continuation of its own. _goto receives a normal function and invokes it with a continuation (KGOTO). Any continuations caught during the execution of f (even by CallCC) can only capture “the future of the computation” upto this KGOTO, since there's nothing above it. Therefore, no matter if f exits normally, or by calling a continuation, it will eventually get to run KGOTO — which takes the next continuation from the stack and applies it on the result.

reset pushes its own continuation (KRESET) to the stack before _goto(th). If it didn't do that, the program would stop when the function exits, because nothing else happens after _goto. So when the function exits, in any way, KGOTO will resume to this KRESET.

Finally, shift calls the function with the KGOTO continuation, so if it exits normally then KGOTO will resume from the top of pstack, and it passes to it a SK function which is able to return to the point where shift itself was called (by using shift's own continuation, KSHIFT). SK is a delimited continuation — it has the ability to return a value, like a function — hence we need to push its k1 continuation to the stack as well. To illustrate the difference I included in this page a shift2 primitive which is just like the shift above but without this line: pstack.push(k1);. Try this sample:

shift gives us a continuation (k) which is the delimited continuation upto the enclosing reset. In this case the continuation is to add 1 to whatever the result of shift was:

1 + [?]

When we're calling k, we're replacing the ? above with a value. We call it twice k(k(2)). We supply 2 and continue the program, hence the inner k(2) will return 3. Therefore what follows is k(3) which supplies a 3, also in place of the question mark, yielding 4 as the final result.

shift2 is incorrect: the inner k(2) never returns.

CallCC-based

As stated, if we had no means to define primitive functions and CallCC was all we had, it'd still be possible to implement delimited continuations. The code below does pretty much the same as the primitives version, but since we don't have JS arrays it uses lists to maintain the stack (as we saw some time back, we can implement lists directly in λanguage, although here they are provided as primitives). The code is a bit longer because we need those CallCC-s in order to fetch the current continuation (which we get as an explicit argument in primitives).

The trickiest part in the code below is how it implements goto, and why it has to do it like this, but I'll leave to you the pleasure of figuring it out.

pstack = NIL;

goto = false;

reset = λ(th) {
  CallCC(λ(k){
    pstack = cons(k, pstack);
    goto(th);
  });
};

shift = λ(f) {
  CallCC(λ(k){
    goto(λ(){
      f(λ(v){
        CallCC(λ(k1){
          pstack = cons(k1, pstack);
          k(v);
        });
      });
    });
  });
};

let (v = CallCC( λ(k){ goto = k; k(false) } )) {
  if v then let (r = v(), h = car(pstack)) {
    pstack = cdr(pstack);
    h(r);
  }
};
Welcome! (login)