Hacker News new | comments | show | ask | jobs | submit login
Coroutine Event Loops in Javascript (syzygy.st)
122 points by kilovoltaire on Dec 12, 2012 | hide | past | web | favorite | 44 comments



I can't believe he doesn't mention task.js: http://www.taskjs.org/

There's tons more potential that this guy describes, too. If you combine this with promises, which is what task.js does, you can immediately start using it in all of your node/client-side code that uses promises to turn your callbacks into `var x = yield getAsyncThing();`.


Good point, that is certainly relevant!

I tried to keep this writeup focused on comparing callback functions to coroutine event loops, especially regarding the differences in how they deal with state.

But I'll add a reference to task.js at the end, once I've tried it out.


The fact that you must call send() on a coroutine to start it is very confusing -- what distinguishes function from a coroutine? The presence of 'yield' inside a function body magically means calling that function doesn't actually return the return value of the function, but a new coroutine which hasn't executed any of its body? This is even more confusing than the function-as-an-object syntax, where at least the new keyword distinguishes the behavior.

I am very much in favor of coroutines in javascript, but I'm kind of surprised by this choice of implementation.


What you have to realize is what we are dealing with here are Generators; a generalized way to create custom Iterators. This article is focusing solely on using Generators for a coroutine system and doesn't elaborate on much so it might seem a bit confusing.

Here's a basic run-down:

You create a Generator Function:

    function NumsUpTo100(start) {
        do {
            yield start;
            start += 1;
        } while (start < 100);
    }
The thing that differentiates this from a regular Function (note: the proposed function* syntax would distinguish it visually as well) is that it actually constructs a new Generator:

    var myGen = NumsUpTo100(20);

myGen now holds a Generator object. You use the arguments to initialize the state of the Generator.

Generators are Iterators, so they really shine in that respect:

    for (var n in NumsUpTo100(50)){
        console.log(n);
    }
However, sometimes you want to manually step through iteration yourself, and you can do that by repeatedly calling .next() on your generator. It will either go forever or eventually throw StopIteration depending on how you wrote it.

AFAIK this article is actually incorrect as you have to call .next() to start the Generator before you can call .send(). Calling send() with no arguments is functionally the same as calling .next(), but last I heard it would throw an error.

The utility of .send() is sometimes during iteration you might want to send information to the generator. For example, maybe you have some sort of state machine in your generator and you want to be able to explicitly reset it's state.

Generators are very useful in general, coroutines is just one fun thing you can do with them.


>Calling send() with no arguments is functionally the same as calling .next(), but last I heard it would throw an error.

I just tried one in Firefox 17, and you can prime it by calling send() with no argument. It objects if there is an argument on the initial send().


Yeah this is a nice advantage of Javascript over Python:

send() is equivalent to send(undefined) which is equivalent to next().

This equivalence is true in Python as well, but you have to explicitly call send(None) which is annoying.


Ah, yes you're right.


I'm not sure what's with OP, but according to [1] (and the example on tasks.js) a generator-producing function should have an asterisk before the name.

  function *myCoroutine() { ... }
1. http://www.2ality.com/2012/07/esnext-classes.html


So, in lieu of the new function* syntax, the presence of the yield keyword in the body is the only thing that distinguishes a generator from a regular function, in the current Firefox implementation?


Cool, I didn't know about that syntax!

Unfortunately it isn't yet supported in Firefox, but I'm glad something like that is in the works.


Hope I didn't offend. I really enjoyed your post; the coroutine syntax seems like an extraordinarily intuitive approach to state machines. Game-changing, maybe. Look forward to trying it.


The Javascript 1.7 coroutine/generator syntax is copied verbatim from Python.

I agree this seems like a weird choice, since Javascript is much more function-oriented than Python.

As for needing to send() first to get it started, this kind of makes sense given that the same syntax is used for generators as well, so there is kind of an off-by-one difference between generators (which return a value from the first send()) and coroutines (which tend not to return values).


I came to post this exact question. I am also curious how coroutines typically work and if this is the expected pattern.


I agree. I assumed that was an error at first, but he kept doing it.


I'm glad javascript people are excited about generators, but I must warn - generators are not a replacement for real coroutines.

Most importantly - generators don't compose.

(I'm talking about python-style generators, the type of generators javascript adapted.)

To give you an idea of what I mean:

    second_sound = coroutine(function() {
        yield;
        console.log('Tick!');
        yield;
        console.log('Tock!');
    });

    hour_sound = coroutine(function() {
        yield second_sound();
        console.log('Ding dong!')
    });
It's pretty obvious what programmer wants to achieve. But with generators you can't call "yield" from deeper-level function - generators are one-level-deep. The only way to compose is to 'yield' a new (deeper) generator. At this point, the caller of hour_sound() needs to understand what to do if the callee returns a generator, instead of 'null' or a valid value. And it needs to run this "deep" generator before doing 'send' on the hour_sound again.

This is just the beginning of the mess. Errors get very hard to track as tracebacks get completely unreadable. It's not easy to decide what to do when one of the nested generators throws an exception.

Python frameworks went through this long time ago, see here (look especially for the "yield" keyword): http://www.tornadoweb.org/documentation/gen.html

Here's a bit of the underlying logic of this "simple" "coroutine-programming-syle" layer of tornadoweb: http://www.tornadoweb.org/documentation/_modules/tornado/gen...

And here's more! https://github.com/facebook/tornado/blob/master/tornado/gen....

All this magic is heavily based on the way python exceptions are integrated with generators, for example one can try to catch "StopIteration" to do a cleanup within generator. I doubt this is possible in javascript as the exceptions mechanism is much less mature.

Edit: Don't get me wrong - I love generators! But one-level-deep generators are not coroutines with stack that can be "blocked" from any deeper function.


Python 3.3 fixes this with the "yield from" keyword:

http://docs.python.org/3.3/whatsnew/3.3.html#pep-380

If Javascript did the same thing, you would write yield from second_sound(); in hour_sound, and then you could run hour_sound as a generator and it would Just Work.


ES6 does the same thing with the `yield*` syntax.


glad to know tornado is creating its own twisted impl.


Twisted has a similar heavy weight machinery to support generator "coroutines" :)


Heavy weight? It's under 40 lines of code. (The inlineCallbacks function here: http://twistedmatrix.com/trac/browser/tags/releases/twisted-...)


"inlineCallbacks" is 3 lines long. "_inlineCallbacks" is around 50 lines long :)

Let's not get into discussion if tornado is better or not than twisted. Both have similar mechanism to flatten generators and both mechanisms are much more complex than one could expect. Also, both of them heavily depend on python exception handling.

BTW, this is brilliant:

    # This function is complicated by the need to prevent unbounded recursion
    # arising from repeatedly yielding immediately ready deferreds.  This while
    # loop and the waiting variable solve that by manually unfolding the
    # recursion.


How useful is this mechanism in practice? It seems very limited to me, because you can't just call a coroutine from another coroutine, and expect it to do its job asynchronously, without the caller having to manually deliver events to it. How can the caller be expected to know what kind of events the coroutine is expecting, and how to deliver them?

What would be useful if this would allow writing asynchronous code in a synchronous style, so that you could do things like:

  function requestHandler (client, request) {
      data = sendToDatabase(request); // "blocking" operation
      client.sendReply(data);
  }

  startServer("0.0.0.0:1234", requestHandler);
  // now server is running and any request that comes in
  // will be handled by requestHandler, and MULTIPLE
  // requestHandler's can be running at the same time,
  // handling different clients.
Am I missing something, or can this be achieved by building on top of the yield primitive?


The value is the way you can control execution: pausing, resuming, reaching in to alter state. That's useful even if everything is synchronous. Sometimes out of desire or necessity you need to do something async, and then you will always have to have some sort of typical callback/async style code...but what you can do with this is control where that code lives and express it in different ways.

In your example, have startServer start a server, then on each request create a generator, keeping a list of the active ones.

Your request handler would typically be written like this:

  function requestHandler (client, request) {
      sendToDatabase(request, function(data){
          client.sendReply(data);
      });
  }
 
What this allows you to do is write something like this:

  function requestHandler (client, request) {
      data = yield sendToDatabase(request, client);
      client.sendReply(data);
  }
Where the callback has been moved to the sendToDataBase function. It would be something like this:

  function sendToDatabase(request, client){
      myDBLibrary.query(request, function(data) {
          Server.clientHandlers[client].send(data);
      }
   }
There's a bunch of different ways you could do it, but that's the general idea.


If you want these JavaScript 1.7 features on the server side, you can use RingoJS (http://ringojs.org/), which has supported 1.7 since 2008 or so (and now sports some 1.8 features) ... It's a much less known alternative for Node.js, better in several ways IMHO (e.g. multithreading).


Does anyone know how V8 is coming along regarding support of JavaScript 1.7?


I can't speak to the overall progress (they've implemented a bunch of stuff I know there are big missing features). As for `yield`, I spoke with the developers a few months ago and they said that it's one of the next big features they'll work on. Don't expect it soon though, I bet it will take a while.

Considering that ES6 has pretty much been approved, and is expected to be finalized in about a year, we will definitely see this in most browsers in a year or two. It's a little ways off, but it's definitely happening.


I'm also very curious about this, because I think node.js is the most reasonable place that this sort of coroutine stuff might actually be useful.

(Generators, meanwhile, are useful all over the place.)


Strictly speaking JavaScript 1.7 is Mozilla's own programming language. V8 does not implement it, instead it implements ECMAScript as described in the ECMA-262 standard. Latest edition of ECMA-262 is 5th and it is fully supported by V8.

Now some features from the 6th edition (that is currently being worked on) are somewhat supported by V8 but they are all hidden behind the --harmony flag.

Generators are actually also in ES6 (though they are a bit different from JavaScript 1.7) so sooner or later they will make their way into V8.

You can star the issue[1] to be notified when the progress is made.

[1] https://code.google.com/p/v8/issues/detail?id=2355


Using generators, you can also convert any callback code into a serial one.

Here's a demo: https://github.com/vjeux/AsyncAwait

Compare

  function main() {
    multiply(1, 2, function (res) {
      multiply(res, 3, function (res) {
        multiply(res, 4, function (res) {
          console.log(res);
        })
      })
    })
  }
  main() // 24
to

  var main = async(function () {
    var [res] = yield await(multiply)(1, 2);
    [res] = yield await(multiply)(res, 3);
    [res] = yield await(multiply)(res, 4);
    console.log(res);
  });
  main() // 24


Why didn't they simplify it (and make it more like current JS) by leaving out the .send() method?

Wouldn't this make more sense?

   var tester = test;
   tester();


I assume you mean:

  var tester = test();
  tester();
And yeah I agree, straight functions seem way nicer, which is what the coroutine() wrapper in the writeup ends up accomplishing.

Javascript 1.7 copied the generator/coroutine objects straight from Python, and it's a weird fit...

One thing that you can't do with just a function is have the .close() method, which may or may not be a fundamental issue.


I mean you could refer to the function by its name, and then run it with () when ready.


Oh, so you really did mean `var tester = test;`

The reason that might be less good than the current approaches is that you may want to spawn multiple 'instances' of the generator/coroutine. If I'm understanding your proposal correctly, you'd only ever be able to run it once.


Oh, that's a good point. Didn't consider that.


This worries me. Imagine some fake code like this:

    function f(x, y) {
      switch(x) {
        case 1: goto a;
        case 2: goto b;
        case 3: goto c;
      }

      a: do_foo(y); return;
      b: do_bar(y); return;
      c: do_other_stuff(y); return;
    }
Calls to it would look like f(1), f(2, "cat"), f(3, "dog"). It's horrible, but at least you see the first arg changing to get some idea of what's going on.

This yield thing makes it possible to call the same function with the same arguments and get completely different behavior depending on how far along it's managed to get.

In that situation, f("cat") != f("cat") != f("cat"), and that just seems scary to me.

(BTW, I am aware that JS doesn't do goto. But this basically adds it in, kinda-sorta.)

Like so many things, I'm sure it can be used for great evil or great justice, but I don't have a whole lot of faith in people to do the right thing with it.


Isn't this just the same situation we already have right now where a function can have different results if one of the hidden variables it closed over has mutated?


I wrote this Erlang-in-Javascript API 5 years ago using coroutines: http://beatniksoftware.com/erjs/. I'm not holding my breath for pervasive `yield` support.


If this sounds like the Next Great Thing, you really want to read the Beazley piece he cites at the end. Beazley comes to the conclusion that you want to use these techniques sparingly. That said, I found Beazley's tutorials well worth working through carefully. He finishes by implementing an operating system kernel in cooperatively-concurrent Python :-).


Coroutines (ability to send values to generators, actually) have been in Python for quite a while, but I'm yet to see a decent use of them... When you add exceptions in the mix things start to get downhill.


yield != coroutine.

if f1 -> f2 -> f3 -> yield, you only go back to f2. Coroutines requires yielding all the way to f1.


so comprehensive!

i wouldn't mind a tl;dr ;?j


Coroutines are functions with bookmarks to remind them where they last left off. A bookmarked infinite loop permits a concise implementation of things like event processing loops.


perhaps the easiest tl;dr is to try: http://syzygy.st/javascript-coroutines/#demo

but as for an explanation: it turns out coroutines let you "invert" memory/variable state into control/flow state!


Nice explanation, thank you. I was indeed wondering if having something like a statefull procedure (if I can call it this way, this is a fairly new concept to me) wouldn't make the code more complex; I didn't actually notice that the state is already there, it was just moved to another place.




Guidelines | FAQ | Support | API | Security | Lists | Bookmarklet | Legal | Apply to YC | Contact

Search: