June 26, 2008

Coroutines, continuations, and call/cc

I'll try and set a precedent for myself by starting out with something that's not full of fluff. The other day, I was doing some reading on parallel languages when I came across a paper extolling the benefits of programming with coroutines. I didn't think the paper was particularly well-written, but it did make it quite clear that I had a hazy conception of what they were. So I resolved to correct this -- by implementing them.

For the uninitiated, coroutines are functions that have stateful control flow and, as a corollary, can have multiple entry and exit points. An example:
(define-coroutine (prefixes)
(display "pre") (yield)
(display "over") (yield)
(display "co") (yield)
(escape))

(define-coroutine (stems)
(display "fix ") (yield)
(display "whelm ") (yield)
(display "routine ") (yield)
(escape))

Assume for the moment that "yield" is essentially like "return" in C. The difference is that when a coroutine returns, it doesn't lose its place. It keeps a continuation to where it left off. This essentially gives it an exit point wherever yield is called and a possible entry point immediately after. So when we run this, we get:
(cobegin prefixes stems)
prefix overwhelm coroutine ...
The coroutines pick up right where they left off. It turns out there are some useful situations for this idiom (think producer/consumer semantics or pipes), and the whole thing struck me as a neat way to make some inroads into playing with dataflow models. So off I went.

I'll spare all of the gritty details, but suffice to say that I went through a handful of iterations, each with their various benefits and drawbacks, but in the end, the example above works flawlessly. I threw all of the code up on my homepage.

No comments: