Futures in JavaScript

May 19, 2015

Callbacks are used for all sorts of things in JavaScript. They're passed around as handlers for successes and failures, as continuations of AJAX and JSONP calls, and much more.

They're also cumbersome to compose.

Futures are simple, but powerful data structures for chaining, combining, and composing callbacks without the need for complex libraries or controversially-implemented language support.

Consider a simple AJAX call:

function getConstants(k) {
  var xhr = new XMLHttpRequest();
  xhr.onload = function () {
    var constants = JSON.parse(this.responseText);
    k(constants);
  };
  xhr.open('get', 'javascript-futures/constants.json', true);
  xhr.send();
}

The file javascript-futures/constants.json contains some constants:

{
  "pi": 3.14,
  "e": 2.72
}

Together, this lets us retrieve the contents of the file, parse it into a JavaScript object containing two values, pi and e, and pass it to a callback k.

Consider also a simple, impure, callback:

var println = function (x) {
  console.log(x);
};

We could use this directly with getConstants to log the constants to the console:

getConstants(println) // logs 'Object { pi: 3.14, e: 2.72 }'

There's not much else we can do with the data, since it winds up in the black hole of println.

Let's start defining a future data structure:

var future = function (f) {
  return {
    apply: function (k) {
      return f(k);
    }
  };
};

Here, a future is a data structure that wraps a function f, which itself needs a callback to continue. Once we have a callback k, we pass it to apply, which calls f with k.

Now we can lift getConstants into a future:

var futureGetConstants = future(getConstants);

We can apply a callback to it, causing the AJAX call to be evaluated:

futureGetConstants.apply(println); // logs 'Object { pi: 3.14, e: 2.72 }'

What if, instead of logging the object itself, we want to pluck a value out of it? Let's add a way to compose pure functions with a future. We'll add a map function:

var future = function (f) {
  return {
    apply: // ...
    map: function (g) {
      return future (function (k) {
        return f(function (x) {
          return k(g(x));
        });
      });
    }
  };
};

If JavaScript had type annotations, map would look something like this:

map : <B>(f: (A) => B) => Future<B>;

Now we can add additional computation to be performed before our callback is applied:

var getPi = function (constants) {
  return constants.pi;
};

var futureGetPi = futureGetConstants.map(getPi);

The same callback can be applied in the same way:

futureGetPi.apply(println); // logs 3.14 to the console

Finally, let's add a way to compose future-returning functions with a future. We'll add a flatMap function:

var future = function (f) {
  return {
    apply: // ...
    map: // ...
    flatMap: function (g) {
      return future (function (k) {
        return f(function (x) {
          return g(x).apply(k);
        });
      });
    }
  };
};

If JavaScript had type annotations, flatMap would look something like this:

flatMap : <B>(f: (A) => Future<B>) => Future<B>;

Now we can compose functions that themselves return futures:

var getE = function (constants) {
  return constants.e;
};

var futureGetE = futureGetConstants.map(getE);

var futureGetEGivenPi = function (pi) {
  return futureGetE.map(function (e) {
    return 'pi: ' + pi + ', e: ' + e;
  });
};

var futureGetPiAndE = futureGetPi.flatMap(futureGetEGivenPi);

futureGetPiAndE.apply(println); // logs 'pi: 3.14, e: 2.72'

Demo

This implementation of future is available in javascript-futures/future.js, and the above examples are available in javascript-futures/demo.js.

Callback: alert

Try these links, and watch the alerts:

Callback: println

Try these links, and watch the JavaScript console: