Disfunctional RSS

Where misquotes, misinformation, and misspellings occur daily.

Archive

Jul
24th
Wed
permalink

Promises, the easy way

Promises are hard. There is an ongoing and heated debate in the node community about the legitimacy of them. The argument basically says callbacks are much easier and promises are unnecessarily complex. Hey! I agree - they are unnecessarily complex.

Among the many proprietary api’s provided by the dozens of promise libraries out there, you’ll typically find methods like then, wait, defer, done, onFulfilled, onRejected, all, execute, and many more!

At loop/recur, we prefer to use promises as functors in most cases. There is an amazing thread full of rage and personal attacks debating why certain promise implementations don’t work with category theory style programming here: https://github.com/promises-aplus/promises-spec/issues/94, but I think we can reap all the benefits of the simple, intuitive api without much philosophical fuss.

Why is a Promise a Functor?


The intuition for functors are that they act like containers with a value in them. In order to do something with the value inside of them you have to (f)map over them.

You can think of a promise conceptually, as having a future value inside of it:

// Array functor
var ax = ["value inside", "other value"]
fmap(function(x){ return x.toUpperCase(); }, ax);
//=>  ["VALUE INSIDE", "OTHER VALUE"]

// Maybe functor
var mx = Maybe("value inside")
fmap(function(x){ return x.toUpperCase(); }, mx);
//=> Maybe("VALUE INSIDE")

var mx = Maybe(null)
fmap(function(x){ return x.toUpperCase(); }, mx);
//=> Maybe(null)

// Promise functor
var px = new Promise();
fmap(function(x){ return x.toUpperCase(); }, px);
//=> Promise()
px.resolve("value inside");
//=> Promise("VALUE INSIDE")
Array runs the function on each value, Maybe only runs the function if it has a value, and Promise runs when the value is actually there. Note that with a promise, the empty Promise() is returned at the time of fmapping. It is only when it gets resolved that the function will run. And even then, it doesn’t actually look like Promise(“VALUE INSIDE”), that’s just the concept of it.

I think a typical temptation at first is try to get the value out of the functor. This isn’t the way to go about it. The functor is what you’re working with - the value inside is only a concept. For instance, in the case of Maybe, you’re really just abstracting a null check. If you try to get the value out, you’ll just have to do null checks manually again from that point on. With a promise, the value has not even arrived at the time of fmapping. All we can do is to tell the functor that we want to run functions on the value inside and let it take care of that for us.

Promise example


I’ll post the implementation at the end, but first, I’d like to just use promises like the math gods intended. In this example, we’d like to get a photo album from the server and change the title of the page to the name of the album.

//+ getAlbum :: {id: Number} -> Promise(Album)
var getAlbum = Http.get('/albums');

//+  updateHtml :: Album -> undefined
var updateHtml = compose($("#title").html, pluck('name'));

//+ changeTitle :: {id: Number} -> Promise(undefined)
var changeTitle = compose(fmap(updateHtml), getAlbum);

changeTitle({id: 3});
Our entry point is changeTitle. This function will call getAlbum which returns us a promise. In order to work with the actual album, we need to fmap over the promise to get the value inside. Intuitive! By partially applying fmap with updateHtml, we get new function that will just “open up” the promise and give the album inside to updateHtml. At this point, updateHtml just works as if it has the album and so we just grab it’s name with pluck and update the html.

Under the hood, the implementation is really just calling those random proprietary api methods like then. Here’s another example of moving a map based on the eventual result of geolocating.

//+ moveMap :: Coords -> undefined
var moveMap = map.setRegion;
                                    
//+ geolocate :: Promise(Coords)
var geolocate = Geolocator.getCoords;

//+ initMap :: Promise(undefined)
var initMap = compose(fmap(moveMap), geolocate);

initMap();
It’s about the same thing. We geolocate, then move the map with the result inside the promise.

Why is this better anyways?


Well, in addition to being simpler and more intuitive, we have a generic application that’s promise agnostic. You could switch the promise library, change to event streams, or even move to synchronous code (with the Identity functor) and in every case fmap will just do the right thing.

Another thing is we’re using a universal abstraction that goes beyond anyone’s personal ideas on what things should be called (it’s not language specific - it’s not even programing specific). When we say fmap or functor, we’re bringing a whole context along with the function. What’s more is we get the functor laws, we can derive a whole bunch of new functions, and we can use promises with any prewritten code that expects a functor. It’s nice to be part of a community that builds upon itself using generic abstractions rather than “read my documentation and start from scratch”. Okay, that was a little much…

Implementation


Here’s a quick implementation using node promises.

var Promise = require('../node-promise/promise').Promise;

var p = new Promise();
var promise_constructor = p.constructor;

// Usually we can just give Promise to our Functor method, but that doesn't work this implementation of promises.
Functor(promise_constructor, {
  fmap: function(f) {
    var promise = new Promise();
    this.then(function(response){
      promise.resolve(f(response));
    });
    return promise;
  }
});

module.exports = Promise;
You can grab the functor library here: https://github.com/loop-recur/typeclasses . I’m seriously adding tests this week and removing globals. I promise…
blog comments powered by Disqus