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:
So let's try it out! If you want to look at more examples, please have a look at the tests.
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 x = yield genlibsomeFunction;var y = yield genObjsomeInstanceFunction;var z = 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""get uri: url 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...
// 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...