Coroutine Event Loops in Javascript
syzygy.st
syzygy.st
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();`.
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.
I am very much in favor of coroutines in javascript, but I'm kind of surprised by this choice of implementation.
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).
function *myCoroutine() { ... }
1. http://www.2ality.com/2012/07/esnext-classes.htmlUnfortunately it isn't yet supported in Firefox, but I'm glad something like that is in the works.
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.
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().
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.
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.
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.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.
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?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.(Generators, meanwhile, are useful all over the place.)
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.
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.
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() // 24Wouldn't this make more sense?
var tester = test;
tester(); 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.
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.
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.
if f1 -> f2 -> f3 -> yield, you only go back to f2. Coroutines requires yielding all the way to f1.
i wouldn't mind a tl;dr ;?j
but as for an explanation: it turns out coroutines let you "invert" memory/variable state into control/flow state!