BAM Weblog

Category Theory for Promises/A+

Brian McKenna2013-04-09

Promises are being debated in the JavaScript community. The most popular specification is Promises/A+. It's a fairly small specification, containing only a single function: then.

The function is heavily overloaded which makes it quite complicated - way more than it has to be. I'll try to show how category theory can give us a much simpler, more generalised and lawful API!

A proper Promise/A+ implementation must provide a then method which always returns a promise. The then method takes two arguments:

Promise.prototype.then = function(onFulfilled, onRejected) {
    // ...
};

The onFulfilled and onRejected callbacks can return a promise or some other value. Both of the callbacks are optional - so let's only focus on the onFulfilled callback for now. If we tried to extract some type signatures it would like something like so:

// then :: Promise a -> (a -> Promise b) -> Promise b
// then :: Promise a -> (a -> b) -> Promise b

Where the top signature is tested for and the bottom is a fallback. Functional programmers might notice those two type-signatures. They come up all the time in Scala and Haskell:

// flatMap :: m a -> (a -> m b) -> m b
// map :: m a -> (a -> b) -> m b

So then is an overloaded flatMap, which falls back to map if the function passed in doesn't return a promise. Now time for some category theory.

Category Theory

The flatMap function is part of being a Monad. The other part is a function with multiple names:

  • point
  • pure
  • return

They all mean the same thing, taking a value and putting it into the monadic context. Putting it together, here's the monad class:

class Monad m where
    flatMap :: m a -> (a -> m b) -> m b
    point :: a -> m a

So does Promises/A+ define a monad? All we'd need is a way to take a value and put it inside of a (fulfilled) promise. Sadly, the spec states:

As with Promises/A, this proposal does not deal with how to create, fulfill, or reject promises.

But what's interesting is that the proposal defines what is a promise:

"promise" is an object or function with a then method whose behavior conforms to this specification.

The specification doesn't define a way of creating a promise - but it does define what a promise looks like. That means we can define our own point function which should hopefully work with other promise libraries:

function point(a) {
    return {
        value: a,
        then: function(onFulfilled) {
            var promise = this;
            setTimeout(function() { promise.value = onFulfilled(promise.value).value; }, 0);
            return promise;
        }
    };
}

(Update: original point implementation was completely wrong. I think the above conforms to part of the spec. It'd need some more details to work when passed to other libraries)

Let's also make a function for treating then like flatMap (i.e. no second argument):

function flatMap(p, f) {
    return p.then(f);
}

So, we've made a promise monad. What does that mean? Before I explain why that's useful, let me talk about the map function above.

If the function we give to then as onFulfilled doesn't return a promise, then it creates a new promise after applying the function. If it didn't have the special case for promises, then it'd be functor:

class Functor f where
    map :: f a -> (a -> b) -> f b

What's impressive is that we can derive a functor using just the point and flatMap that we defined above!

function map(p, f) {
    return flatMap(p, function(a) {
        return point(f(a));
    });
}

The trick is that point will even make a promise out of a promise!

So monads are useful for defining functors, what else are they good for? Let's imagine a promise library gave us a nested promise (a promise within a promise). We can write a function to flatten it:

function identity(a) {
    return a;
}

function join(p) {
    return flatMap(p, identity);
}

The above will work for any monad. List of lists? Optional optional value? If the JavaScript community settled on using flatMap as a method, we could write DRY, generalised code for monads.

Above I showed how we can get a functor from any monad. We can do one better; all monads form applicative functors:

class (Functor f) => Applicative f where
    point :: a -> m a
    ap :: m a -> m (a -> b) -> m b

Here's how to derive the ap function:

function ap(p, pf) {
    return flatMap(pf, function(f) {
        return map(p, f);
    });
}

Applicatives have some really useful functions:

// liftA2 :: f a -> f b -> (a -> b -> c) -> f c
function liftA2(pa, pb, f) {
    return ap(pb, map(pa, function(a) {
        return function(b) {
            return f(a, b);
        };
    }));
}

This is very useful for promises. It allows use to run multiple promises and run a function when they're all finished:

liftA2(readFile('hello.txt'), readFile('world.txt'), function(hello, world) {
    console.log(hello + ' ' + world);
});

Lots of promises libraries write special functions to achieve the above magic. But again, we can be more DRY because this stuff works for all applicatives!

We could even combine the above with operator overloading for a nice DSL.

Recommendations

We've only been focusing on the onFulfilled case - we left out the onRejected case. What should we do with that?

I think that should just be an additional method on promises - not on an overloaded flatMap:

Promise.prototype.onRejected = function(callback) {
    // ...
};

(Scala provides an onFailure method to do the same thing)

It seems like the JavaScript community has settled on then as being the name for flatMap (that's fine, in Haskell it's called bind or >>=). I think then should only take the onFulfilled function and it should have unspecified behaviour if the function doesn't return a promise (for simplicity; map can be derived).

I also think the specification should define a way of creating promises. It's only a few lines to create a compatible promise but I don't think that should be up to the library's users.

And lastly, it would be nice to have a done method which forked the promise. That would allow the API to be purely functional. No effects would happen until done was called. Turning side-effects into an effect. Sadly, I think the JavaScript community would not appreciate this approach.

So we should have something which looks like this:

Promise.point = function(a) {
    // ...
};
Promise.prototype.then = function(onFulfilled) {
    // ...
};
Promise.prototype.onRejected = function(callback) {
    // ...
};
// Possibly
Promise.prototype.done = function() {
    // ...
};

Just having the two functions of the monad interface allows us to derive functions to map over the promise, flatten a promise, run a function when promises are finished and much more. These functions will also work with other monads or applicative functors if we rely on point and then as an API for different structures.

Summary

Recognising that promises are monadic gives us an API which allows us to derive lots of useful functions. These functions are based off of category theory which also means that they work with not just promises - allowing the API to be consistent and DRY for many structures.

I think we should apply that knowledge directly to a promises specification. An implementation would only require three very simple functions:

  • point(a)
  • then(f)
  • onRejected(f)

No surprising overloaded behaviour or optional arguments. It would be compatible with some existing libraries.

I believe that the JavaScript community needs to act quickly to achieve a more consistent and simple API before promises become solidified. The time is now!

Please enable JavaScript to view the comments powered by Disqus.