Asynchronous coding using ECMAScript 6 generators
ECMAScript 6 introduces something called generators. They look like this:
yield "SomeValue";yield "SomeOtherValue";var a = myGenerator;var value = anext; // first line in myGenerator executes, returns "SomeValue"var otherValue = anext; // second line in myGenerator executes, returns "SomeValue"
Ok...that looks pretty much like Python/Scala/C#/whatever. What does that bring in terms of asynchronous code? Well, the idea is that if we can write our code as a generator generating different asynchronous pieces of code, we can use the built-in wrapping/unwrapping of function bodies and try/catch statements to make our life easier. We could write something like
tryvar todosTask = fetchUrl"/todos";var emailTask = fetchUrl"/todos";// Wait for the two parallel tasks to finishvar todosAndEmail = yield todosTask emailTask;console.log"All fetched" todos email;catch econsole.error"Oops...something went wrong" e;
Note that the generators return objects that we have to "yield" to see the result for. If you get that, you get what it's about.
And to just make the "why yield" answer a little clearer:
- Ever used an async library and just get lost. Where did that call go? No reply, no error, no nothing. Rescue is under way.
- We can use try/catch again. You've read the posts about avoiding those pesky keywords in asynchronous code (i.e. all code) as you can't rely on them being called. But hey, remember that convenient idea of wrapping a bunch of calls in try/catch and handling a lot of different errors in a grouped way for a piece of code. Perhaps being able to send an error back or outputting to some log. Sure, there are solutions in old callback land such as node domains, load balancing workers, and it may be a good idea to die rather than to do stupid things. But, being pragmatic, it's pretty nifty to be able to actually catch all errors within a block of code and decide for yourself.
- Ever felt a little bad about cluttering your objects, parameters and classes with callbacks here and there. Get ready for cleaner code.
- Ever written some asynchronous code and made a mistake in the error handler? Maybe you forgot to add one? Maybe your colleague did? Maybe you typed it incorrectly and now your application has just not returned from a call in quite some time. Console is just blank. :(
- Asyncronous stack traces? It is pretty saddening to just see that EventEmitter in your stack trace, right? With that said, there are node packages to make it easier such as trycatch.
- ECMAScript is in a way catching up with this. Async handling has been major recent lanaguage features in languages such as C#, F#, Scala,
So let's try it out! If you want to look at more examples, please have a look at the tests.
- Install node js, minimum 0.11.2, but preferred 0.11.4, i.e. experimental branch. NOTE: 0.10.x branch does NOT work yet.
- Remember to run with "node --harmony" !
- In your scripts, use: var Y = require("yyield");
- Install Chrome canary from https://www.google.com/intl/en/chrome/browser/canary.html
- First, include underscore.js or lodash.js
- After that script include, include yyield. AMD is not implemented yet. Please use either:
...or require("yyield") with [RequireJS](http://requirejs.org/) and r.js
- Now Y-yield is accessible through window.Y. If something else was previuosly attached you can reach it at window.Y.noConflict
I will add supprt and tests as those browsers support generators. At present, they don't
// Here we run our async programrun;
You can "yield" all sort of stuff to make life easier, e.g.:
yield setTimeout cbnull; timeout;
yield setTimeout cbnull; timeout;yield sleep1000;run;
The above example then becomes
return setTimeout cbnull; timeout;return sleep1000;run;
You can convert objects or functions by using the exported "gen" function. This assumes that all functions have the format
...where cb is a callback on the form callback(error, [resultArguments])
var Y = require"yield";var lib = require"somelib"var genlib = Ygenlib;var genObj = Ygen;var genFunc = YgenlibsomeFunction;var a = yield genlibsomeFunction;var b = yield genObjsomeInstanceFunction;var c = yield genFunc;run;
Note that when converting an object, "this scope" is preserved. It is not when you convert a single function. Also - conversions are shallow (just one level of functions) and return values are not converted. Thus - if you require a library which exports a class that you construct, by using
var myClassInstance = ;
...then you also have to convert the myClassInstance to use generators, by using
var genMyClassinstance = require"yield"genmyClassInstance;
By using promises based asynchornous flows, you are able to chain calls with multiple calls to .then() in e.g. Q or jQuery. You can mix this with calls to done/fail to create way to accomplish asynchronous data flows.
Read more about Q at https://github.com/kriskowal/q Read more about jQuery deferreds at http://api.jquery.com/jQuery.Deferred/
Here's an example using jQuery Deferred (namely the quite common return object form $.ajax/getJSON)
tryvar newTodos = yield $getJSON"/todos/new";alertnewTodoslength + " new todos found.";catcheconsole.errorestack;run;
Y also integrates with these by returning promises from the run method. Note that promises are only returned if you're running in node or requirejs (by using Q) or if you're running in a browser and jQuery exists. Y-yield does not require that Q or jQuery are installed and will work fine without them - only run will not return anything. Here's an example where we use Y-yield to chain on a then function:
// See fetchUrl in example abovereturn fetchUrl"/todos";runthenconsole.log"Here are the todos" result;
This then gets executed in parallel. Example:
// requires "npm install request-json"var JsonClient = require'request-json'JsonClient;return""geturl cb;// Our generator async program// This gets executed in parallelvar todosAndEmails = yield fetchUrl"/todos" fetchUrl"/email"run;
// See fetchUrl in example abovevar lazyTodos = fetchUrl"/todos";// Will be fetched the first time getTodos is called, but only the first timereturn lazyTodos;
// See fetchUrl in example above// By calling "run" on the iterator, we fire it off directly. Here we fetch both todos and emailsvar todos = fetchUrl"/todos"run;var emails = fetchUrl"/emails"run;// Finally wait for todos and e-mails. If we hadn't called next above, these calls would "kick it all off"var todosResult = yield todos;var emailsResult = yield emails;// Do something with todos and emails here...
Y-yield overrides a couple of underscore/lodash functions to make them generator aware so that you can use them with generators. Currently - the following functions are supported:
- each/forEach - runs sequentially lodash docs/underscore docs
- map - runs in parallel lodash docs/underscore docs
- filter/select - lodash docs/underscore docs
- reject - runs in parallel - lodash docs/underscore docs
// See fetchUrl in example above// By calling "next" on the iterator, we fire it off directly. Here we fetch both todos and emailsvar todos = fetchUrl"/todos"run; // Calling built-in ".next()" would work just fine toovar email = fetchUrl"/todos"run; // Calling built-in ".next()" would work just fine too// Set up a handler "in the future". This will be called once todos has arrivedvar todosWithExtra = _todosmapvar extra = yield fetchUrl"/todos/" + todoid + "/extra"return _todoextendextra;;// Set up a handler "in the future". This will be called once email has arrivedvar emailsWithExtra = _todosmapvar extra = yield fetchUrl"/emails/" + todoid + "/extra"return _emailextendextra;;// Finally wait for todos and e-mails. If we hadn't called run above, the yield calls below calls would "kick it all off"var todosResult = yield todosWithExtra;var emailsResult = yield emailsWithExtra;// Do something with todos here...