JS control FLOW


Tom Yandell <tom@yandell.me.uk>

THE FUTURE AWAITS




"ES7 async functions - They're brilliant. They're
brilliant and want laws changed so I can marry them."

Jake Archibald (http://jakearchibald.com/2014/es7-async-functions/)

What's the big deal?


Traditional control flow:


from requests import get

location = get('http://www.telize.com/geoip').json()
lat = location['latitude']
lon = location['longitude']

weather = get('http://api.openweathermap.org/data/2.1/find/city?lat=' + str(lat) + '&lon=' + str(lon) + '&cnt=1').json()

print weather['list'][0]['weather'][0]['description']

Great until you want to do things concurrently...

WE NEED TO TALK ABOUT THREADS




NO!


(concurrent access to mutable state is the road to insanity)

CALLBACK CONTROL FLOW


var get = require('request').get;

get({ url: 'http://www.telize.com/geoip', json: true }, function (e, r, loc) {
    var weatherUrl = 'http://api.openweathermap.org/data/2.1/find/city?lat='
        + loc['latitude'] + '&lon=' + loc['logitude'] + '&cnt=1';

    get({ url: weatherUrl, json: true },  function (e, r, weather) {
        console.log(weather['list'][0]['weather'][0]['description']);
    });

    // as get didn't block, I could do something else here at the same time
    // as the weather is fetched
});
// or here

Control flow is now left to right, rather than top to bottom. You get used to it, but it kinda sucks :-(

A promising Solution


var get = require('q-io/http').read;

var getLocation = get('http://www.telize.com/geoip');

var getWeather = getLocation.then(function (location) {
    location = JSON.parse(location);
    return get('http://api.openweathermap.org/data/2.1/find/city?lat='
           + location['latitude'] + '&lon=' + location['logitude'] + '&cnt=1');
});

getWeather.then(function (weather) {
    weather = JSON.parse(weather);
    console.log(weather['list'][0]['weather'][0]['description']);
}).done();

Top to bottom control flow is regained, but it's easy to slip back into nested "then" callbacks. 

A sprinkle of sugar...


var get = require('q-io/http').read;

async function printWeather () {
    var location = JSON.parse(await get('http://www.telize.com/geoip'));

    var weather = JSON.parse(await get(
        'http://api.openweathermap.org/data/2.1/find/city?lat=' +
        location['latitude'] + '&lon=' + location['logitude'] + '&cnt=1'
    ));

    console.log(weather['list'][0]['weather'][0]['description']);    
}
So new, even the syntax highlighter doesn' t support it.
C#, Perl 6, etc. etc. eat your heart out.

Going native

Promise libraries often add a done method to their promises. This isn't in the Promises/A+ spec, but is needed for confidence that you've handled errors in your promise chain. It has an implementation that's logically equivalent to:
Promise.prototype.done = function (callback, errback) {
    var promise = callback || errback ? this.then(callback, errback) : this;
    promise.then(null, function (error) {
        throw error;
    });
};
If ES7 makes it possible to await outside of async functions, then the the promise returned by an async function can simply be awaited with errors caught with try and catch, making the done method redundant (https://github.com/lukehoban/ecmascript-asyncawait/issues/9).

I WANT IT NOW!

Option 1: compile to currently supported
JavaScript with the Traceur transpiler.

Not sure I want to buy into the entire Traceur
runtime, and maybe the code it generates
will be hard to debug.

Possibly FUD so choose your own path.


generation game

Option 2: The yield keyword has very similar semantics to await and can be used to achieve the same thing:
var coro = require('coro'),
    get = require('q-io/http').read;

coro.run(function * () {
    var location = JSON.parse(yield get('http://www.telize.com/geoip'));

    var weather = JSON.parse(yield get(
        'http://api.openweathermap.org/data/2.1/find/city?lat=' +
        location['latitude'] + '&lon=' + location['logitude'] + '&cnt=1'
    ));

    console.log(weather['list'][0]['weather'][0]['description']);    
}).done();
Very easy to translate from the await version, very easy to translate back when native support arrives. Lots of other libraries that do similar, but this one is mine (https://www.npmjs.org/package/coro).

TRANSPilATION

Okay, pretty sure that isn't a word, although it nicely reflects the ugliness of introducing a compile step for JavaScript.

Unfortunately Harmony generators are not well supported either, so transpiling is currently unavoidable. Native support is available in Node.js 0.11.x with the --harmony flag, and in Chrome if you enable it.

:-(

Chill out, dickwad

Okay, so we can use it natively in Node and transpile for the Browser. That doesn't sound too bad.

Want a transpiler that does the minimum possible, produces the most understandable code and has a small runtime footprint. Thankfully some smart people at Facebook have produced just that:

I'll be back

If you're okay with transpilation until await shows up, then a version of regenerator that supports async/await looks imminent:

Never send a human

to do a machine's job

Many tools for making use of async/await today:

FIN

JS Control Flow

By tomyandell

JS Control Flow

  • 3,086