Demystifying Continuations in Ruby (because they shouldn’t be feared)

By Loren Segal on March 21st, 2009 at 1:35 PM

So most people won’t touch continuations with a 10 foot pole, that’s probably why they’ve been left out of JRuby and pseudo-deprecated in Ruby 1.9 (YARV performance was also an issue here, to be fair) by being pushed out to an explicit “require ‘continuation’” call. Perhaps it’s been misunderstood, but there’s a secret that nobody knows about continuations: they’re dead simple. BRAINDEAD simple.

Most blogs and articles I’ve seen that try to explain continuations get it wrong. They bore you with low level details that confuse you in the first place, or scare you by starting with the premise that “it’s complicated, but I’ll try to make you understand”. I’m not going to do either, I’m actually going to explain continuations in two words, and I want you not to laugh, cry, or vomit on yourself when you read them:

GLORIFIED GOTOs

Oh no, I just said a dirty word, didn’t I. You may have your own irrational fears of the goto statement, but you probably know what they are and how they work:

#include <stdio.h>
int main() {
  int i = 0;
  printf("%d\n", i);
  goto some_label;
  i = 1;
  some_label:
  printf("%d\n", i);
  return 0;
}

The above snippet of working C shows how we can jump to different points in a function. It prints 0 twice instead of 0 followed by 1, as you would expect. Language evolution has mostly deprecated this form of programming through method calls and, more intricately, exception handling. These new practices are really just syntactic sugar on the old goto concept. Hopefully all those who scoff at any mention of goto will one day sit down and realize that they’re a central part of how computers work, but I’m getting off topic. The point is, whether you use gotos or not, they’re extremely simple to understand. In fact, they’re exactly as simple as continuations; let’s re-write the above using those “scary” guys:

def main
  i = 0
  callcc do |label| # callcc gives us ‘label’, a continuation object
    puts i
    label.call # this is our "goto" statement
    i = 1      # we skip this completely  
  end          # this is where our "label" would otherwise sit
  puts i
end

To those in the know, this doesn’t really show off the full-extent of callcc’s powers (that comes later), but you can look at "label.call" as the equivalent of "goto label", and the end of the callcc block is where the label would be. That’s basically how continuations work.

If you’re having trouble parsing the above: the little trick to understanding the code is simply realizing that the block that callcc yields is executed immediately, once, and only once. It’s simply there so you can do any one-time initialization with the continuation you’re creating. The end of the block is where any subsequent calls to the yielded continuation object will go, not the block itself.

NOW YOU KNOW.

So what, is it really that simple? Continuations are another way to write gotos? The answer is: pretty much. There are actually only two functional difference that makes them even more powerful than the above C-style gotos, but one that makes them a little less-so.

More Powerful, How?

1. They’re not local to your method. Simply put, we can’t do the following in C:

// a() is called by main()
void a() { 
  printf("hello world\n");
  label1:
  printf("then you say...\n");
  b();
}
void b() {
  printf("then I say...\n");
  goto label1;
}

This should print "hello world" followed by "then you say", "then I say", in a never-ending loop. Problem is, it won’t compile. Without getting into the nitty-gritty of why C can’t do this, I’ll just simply show how we can actually get away with it using continuations:

def a
  puts "hello world"
  callcc {|cc| $label1 = cc } # pretend this says "label1:"
  puts "then you say..."
  b
end

def b
  puts "then I say"
  $label1.call # pretend this says "goto label1"
end

As the comments indicate, this is almost a one-to-one translation of the C code above. Our continuation object is set to a global variable, essentially making it a “global label” that can be goto’d by running the method #call (picture it as “label.goto” instead of #call).

2. They maintain stack-frame state. That’s the reason why labels in C are local to a function, because you can’t just jump between functions without switching stack frames. Actually, there are tricks in C to do just that (namely longjmp and setjmp to jump to arbitrary addresses and continue execution which is actually how continuations are done in C), but we’re focusing on Ruby here, right? Basically, the continuation object yielded from callcc (the {|cc| thing}) contains a snapshot of our stack frame at the point it was yielded at (remember, the block is only yielded that first time). This allows us to jump between methods in a class, between classes, and jump back from a nested call when we are arbitrarily deep in a stack frame.

Anyone Up For A Quick Real World Example?

The last situation is the one with some real world usage. Imagine we’re writing a web framework with a Rails-style before_filter and we want the ability to halt the execution during a filter and jump all the way back up to the router code to find the next matching route. Sinatra (sort-of) does this with the `pass` method, and though it probably doesn’t use continuations to achieve the result, the concept here is a perfect example of wanting to jump back through our stack frames arbitrarily. We could be 3 method calls deep, or we could be 20 method calls deep, but we need to “go to” a specific point in our program execution. Usually, people implement this with try/catch style exception handling (RouteAbortError maybe), but continuations may be a little cleaner depending on the scenario (we might have a method handling the exception 3 method calls up, but we want to jump 7 method calls up to the initial point in code).

Less Powerful, How?

There’s one thing you may have noticed from the initial example, it’s that jumping forward looks different from the example where we jumped backwards. The limitation is that continuations can’t actually jump forwards, at least not between method calls. The reason is that in C, labels are compiled into the program statically, but in Ruby, the continuation objects are created at runtime. This means that we’d need to execute that callcc call in our future method to generate the continuation, but we can’t run code that hasn’t yet been run. In short, continuations are great for jumping back in time, not quite so for jumping forwards.

Final Correction

So I initially called continuations “glorified gotos”. Hopefully this got you to understand the simple concept behind their use; but now I should fess up and modify the definition just a little bit to be slightly more accurate. Instead of glorified gotos, think of continuations as:

GLORIFIED, STATEFUL, BACKWARDS JUMPING, GOTOs

Now that you know what the fuss is about, you can decide to hate continuations all you want.


Comments