Asynchronous Javascript – Promises and Beyond



Asynchronous Javascript – Promises and Beyond

1 1


presentation-asyncjs

Presentation on working with asynchronous javascript (promises, async/await, functional programming, reactive programming)

On Github yamalight / presentation-asyncjs

Asynchronous Javascript

Promises and Beyond

Outline

Intro Callbacks Promises Bluebird.js Async/await Event streams Functional Programming Functional Reactive Programming When to use what

Intro

So, what's the big deal?

Synchronous code

    // Your typical synchronous code
    var getQueryResult = function (query) {
      var result = execQuery(userId);
      return result;
    }

    var result = getQueryResult('SELECT ?s WHERE { ?s ?p ?o }');
    contaner.render(result);
  

Asynchronous code

    // Both execQuery & getQueryResult are async
    var getQueryResult = function (query, cb) {
      execQuery(query, function(res) {
        res = res.toArray();
        cb(res);
      });
    }

    getQueryResult('SELECT ?s WHERE { ?s ?p ?o }', function(res) {
      container.render(res);
    });
  

Looks ok, what's the problem?

    var getQueryResult = function (query, cb) {
      execQuery(query, function(res) {
        expandResults(res, function(expandedRes) {
          filterResults(res, function(finalRes){
            finalRes = finalRes.toArray();
            cb(finalRes);
          });
        });
      });
    }

    getQueryResult('SELECT ?s WHERE { ?s ?p ?o }', function(res) {
      container.render(res);
    });
  

The Pyramid of Doom

Better asynchronous code

slightly better?

    var processFinal = function (cb, finalRes){
      cb(res);
    }
    var processExpandedResult = function (cb, expandedRes) {
      filterResults(res, processFinal.bind(this, cb));
    }
    var processQueryResult = function (cb, res) {
      expandResults(res, processExpandedResult.bind(this, cb));
    }
    var getQueryResult = function (query, cb) {
      execQuery(query, processQueryResult.bind(this, cb));
    }
    getQueryResult('SELECT ?s WHERE { ?s ?p ?o }', function(res) {
      container.render(res);
    });
  

Still messy

Error handling

synchronous way

    var getQueryResult = function (query) {
      var result = execQuery(userId);
      return result;
    }

    var result = getQueryResult('SELECT ?s WHERE { ?s ?p ?o }');
    if(result.error) {
      return container.error(result.error);
    }
    container.render(result.data);
  

Error handling

asynchronous way

    // Both execQuery & getQueryResult are async
    var getQueryResult = function (query, cb) {
      execQuery(query, function(err, res) {
        if(err) {
          return cb(err);
        }
        res = res.toArray();
        cb(null, res);
      });
    };

    getQueryResult('SELECT ?s WHERE { ?s ?p ?o }', function(err, res){
      if(err) {
        return container.error(err);
      }
      container.render(res);
    });

Callbacks are ...

Easy to learn but do not scale very well (get complex very quick)

Promises

What is a Promise?

  • Object that provides .then method
  • Promise can be pending, fulfilled or rejected
  • .then takes two functions as arguments: onFulfilled, onRejected
  • .then must return a promise
  • Promise can be fulfilled only once

See full spec for Promises/A+ on github.

Accessing the result

    var queryPromise = getQueryResult('SELECT ?s WHERE { ?s ?p ?o }');

    // Some time later

    promisedPic.then(function(res) {
      container.render(res);
    });
  

It doesn't matter if it has been fulfilled yet or not

Chaining

    getQueryResult('SELECT ?s WHERE { ?s ?p ?o }')
    .then(function(res) {
      return filterResults(res)
      .then(function(filteredRes) {
        container.render(filteredRes);
      });
    });
  

Chaining

no such thing as a nested promise

    getQueryResult('SELECT ?s WHERE { ?s ?p ?o }')
    .then(function(res) {
      return filterResults(res);
    })
    .then(function(res) {
      container.render(res);
    });
  

Error handling

    getQueryResult('SELECT ?s WHERE { ?s ?p ?o }')
        .then(function(res) {
          return filterResults(res);
        })
        .then(function(res) {
          container.render(res);
        },
        function(err) {
          contaner.error(err);
        });
  
    getQueryResult('SELECT ?s WHERE { ?s ?p ?o }')
    .then(function(res) {
      return filterResults(res);
    })
    .then(function(res) {
      container.render(res);
    })
    .catch(function(err) {
      container.error(err);
    });
  

Promises/A+ libraries

Bluebird.js

Fully featured promise library with focus on innovative features and performance

Bluebird features

the basics

  • Implements Promises/A+ 2.0.2 (with additional things like progression and cancellation)
  • .bind() method that allow currying promises
  • Has collection methods (all, any, map, etc)
  • Has practical debugging solutions such as unhandled rejection reporting, typed catches, catching only what you expect and long stack traces
  • Sick performance

Bluebird features

awesome stuff

  • Promise.promisify
  • Promise.promisifyAll
  • Promise.nodeify
  • Promise.coroutine (ES6)

Promise.promisify

  var readFile = Promise.promisify(require("fs").readFile);

  readFile("myfile.js", "utf8")
  .then(function(contents){
      return eval(contents);
  }).then(function(result){
      console.log("The result of evaluating myfile.js", result);
  }).catch(function(e){
      console.log("Error reading file", e);
  });
  

Promise.promisifyAll

  var fs = Promise.promisifyAll(require("fs"));

  fs
  .readFileAsync("myfile.js", "utf8")
  .then(function(contents){
      console.log(contents);
  }).catch(function(e){
      console.error(e.stack);
  });
  

Promise.nodeify

  function getDataFor(input, callback) {
    return dataFromDataBase(input).nodeify(callback);
  }
  

Async/await

Turning asynchronous into synchronous

Generators and yield (ES6)

task.js (Mozilla)

  spawn(function*() {
    var data = yield $.ajax(url);
    $('#result').html(data);
    var status = $('#status').html('Download complete.');
    yield status.fadeIn().promise();
    yield sleep(2000);
    status.fadeOut();
  });
  

Promise.coroutine (ES6)

  function delay(ms) {
      return new Promise(function(f){ setTimeout(f, ms); });
  }
  function PingPong() {}
  PingPong.prototype.ping = Promise.coroutine(function* (val) {
      console.log("Ping?", val)
      yield delay(500)
      this.pong(val+1)
  });
  PingPong.prototype.pong = Promise.coroutine(function* (val) {
      console.log("Pong!", val)
      yield delay(500);
      this.ping(val+1)
  });
  var a = new PingPong();
  a.ping(0);
  

asyncawait (ES5)

Built using node-fibers and Promises.

    var getQueryResult = function (query, cb) {
      execQuery(query, function(res) {
        expandResults(res, function(expandedRes) {
          filterResults(res, function(finalRes){
            cb(res);
          });
        });
      });
    }
    getQueryResult('SELECT ?s WHERE { ?s ?p ?o }', function(res) {
      container.render(res);
    });
  var getQueryResult = async(function(query) {
    var res = await(execQuery(query));
    var expandedRes = await(expandResults(res));
    var finalRes = await(filterResults(res));
    return finalRes;
  });
  getQueryResult('SELECT ?s WHERE { ?s ?p ?o }').then(function(res) {
    container.render(res);
  });

Functional programming

Composing functions

synchronous, procedural

    var getQueryResult = function (query) {
      var res = execQuery(query);
      finalRes = processResults(res);
      container.render(finalRes);
    }

    getQueryResult('SELECT ?s WHERE { ?s ?p ?o }');
  

Composing functions

synchronous, fuctional

    var getQueryResult = compose([
      execQuery,
      processResults,
      container.render.bind(container)
    ]);

    getQueryResult('SELECT ?s WHERE { ?s ?p ?o }');
  

Composing functions

asynchronous, procedural

    var getQueryResult = function (query, cb) {
      container.showProgressIndicator();
      execQuery(query, function(res) {
        processResults(res, function(finalRes) {
          container.render(finalRes);
          cb();
        });
      });
    }

    getQueryResult('SELECT ?s WHERE { ?s ?p ?o }', function() {
      container.hideProgressIndicator();
    });
  

Composing functions

asynchronous, higher order functions

  var getQueryResult = composeAsync([
      perform(container.showProgressIndicator.bind(container)),
      execQuery,
      processResults,
      perform(container.render.bind(container))
  ]);
  getQueryResult('SELECT ?s WHERE { ?s ?p ?o }',
    container.hideProgressIndicator.bind(container));
  
  var getQueryResult = compose([
    execQuery,
    processResults,
    container.render.bind(container)
  ]);
  

Same structure as sync

No need to change paradigm

Syntax sugar

a matter of taste

    // If you like jQuery or promises chains
    var getQueryResult =
        perform(container.showProgressIndicator.bind(container))
            .map(execQuery)
            .map(processResults)
            .perform(container.render.bind(container));

    getQueryResult
        .perform(container.hideProgressIndicator.bind(container))
        ('SELECT ?s WHERE { ?s ?p ?o }');
  

Frameworks

Async.JS

Composing with Async

  // Reversed order
  var getQueryResult = async.compose(
      execQuery,
      processResults,
      // perform not in Async
      perform(container.showProgressIndicator.bind(container))
  );
  getQueryResult('SELECT ?s WHERE { ?s ?p ?o }', function (err, pic) {
    // Node convention
    if(err) {
      return container.showError(err);
    }
    container.render(pic);
  });
  

Async has almost everything you might need

  • Transform & Filter
  • Temporal logic
  • Synchronization
  • Merging
  • Grouping
  • Unwrapping streams of streams
  • And more...

(Node) Event streams

IO everywhere

Node streams

efficient IO

    // Streams are EventEmitters
    dataStream.on('data', function(data) {
      // Consume data chunk
    });
    dataStream.on('error', function(err) {
      // Handle errors
    });
    dataStream.on('end', function() {
      // No more data, stream ended
    });
    // With a pipe method
    dataStream.pipe(compressor('zip')).pipe(res);
  

Want to know more? Stream Handbook

Event Streams

  • Readable streams are event sources
  • Writable streams are side effects
  • Through streams are async transformations

Event-stream project

Live querying with Event Streams

simplified version

  var es = require('event-stream');
  var domstream = require('domnode-dom');
  var queryStream = es.pipeline(
      domstream.createReadStream('input.query', 'keyup'),
      es.mapSync(trim),
      es.mapSync(toLowerCase),
      es.mapSync(function(text) {
        container.showProgressIndicator();
        return text;
      }),
      es.map(getQueryResult),
  );
  queryStream.on('data', container.render.bind(container));
  queryStream.on('error', container.error.bind(container));
  queryStream.on('end', container.hideProgressIndicator.bind(container));
  

Strong points

  • Compatible with all node streams libs
  • Easy to extend
  • A lot of different streams out there

Weak points

  • Node centric
  • Need to use Browserify or similar
  • More docs!

(Functional) Reactive Programming

The best of both worlds

  • Data Source = Streams + FP tools
  • First citizen objects
  • Coherent set of operations
  • Coherent language
  • Chainable & composable (Promise like)

Two kinds of Data Sources

1. Data sequences

  • Have memory (Promise like)
  • Remember N last values
  • Replay N last values
  • Useful for data processing & event sourcing

Two kinds of Data Sources

2. Event streams

  • Do not have memory
  • Useful when we don't care about past info

Promises VS. FRP

  • Property: data sequence that remembers only the last value
  • Promise: a property with only one single value and event
  • Promises are a subset of FRP

RX

from Microsoft

  • Bridges with some of the existing tech
  • Medium weight (12 KB min+gzip)
  • LINQ centric
  • Have its own "language"
  • Apache License & Microsoft Open Technologies

Netflix - Async JavaScript with Reactive Extensions

Bacon

  • Made for JS ™
  • Node & Browser
  • Lightweight (5 KB min+zip)
  • Not as big as RX, very focused
  • Only bridges with jQuery/Zepto
  • Event Streams & Properties

Live Querying with Bacon.JS

simplified version

  var queryText = $('input[type="text"][name="query"]')
    .asEventStream('keyup')
    .map(".target.value")
    .map(trimText)
    .toProperty(""); // memory !

  queryText
    .sample(500)
    .skipDuplicates()
    .filter(shorterThan(3))
    .map(getQueryResult)
    .onValue(function (res) {
      queryText.on('data', container.appendResult.bind(container));
      queryText.on('end', container.end.bind(container));
    });
  

When to use what?

General rule!

K.I.S.S.

Use the most simple tool that can solve your problem.

When to use Callbacks

simple logic

  • Simple async logic: at most 2 async transformations.
  • No time to learn anything more complex.
  • Small code base or proof of concepts.

When to use Promises

  • There's need to chain async processes
  • No need to use fancy incremental processing

When to use FP or FRP

when promises are not enough

  • In use cases when promises don't help
  • Incremental processing, UI logic, etc.

Thank you!

Questions?