A Challenge Problem for Promise Designers

Tab Atkins Jr. jackalmage at gmail.com
Sat Apr 27 09:47:16 PDT 2013


On Sat, Apr 27, 2013 at 9:21 AM, Ron Buckton <rbuckton at chronicles.org> wrote:
> Here is a case where flattening helps:
[snip example code]
> In this example, the server will receive a command from the client and will
> process it asynchronously. The client then needs to poll an endpoint to
> check for completion of the command. Just using Futures, flattening is
> helpful here. Without flattening, executeAndWaitForComplete would could
> return either a Future<object> OR return a Future<Future<object>>.

Nope, this is the same error that David Bruant was making in his arguments.

In the "monadic promise" proposal (as opposed to the "recursive
unwrapping" proposal), a .then() callback can return a plain value *or
a promise for a plain value*, and the output promise returned by
.then() will be *the exact same*.

In other words, in your example code, executeAndWaitForComplete() gets
a promise from getJSON(), then calls .then() to chain some code after
it and returns the resulting promise.  The .then() callback either
returns commandResult.model (a plain value), or returns a promise
generated from pollForCommandComplete() (which eventually completes
with pollResult.model).

The only difference between the two code branches in .then() is that
the first one (where it returns the model) accepts immediately with
the model value, while the second one (where it returns a promise for
the model) stays pending for a little while long, and then eventually
accepts with the model value.

To anything calling executeAndWaitForComplete(), the return result is
*always* Promise<model>.  It is *never* Promise<Promise<model>>.
That's not something that can happen, because promises obey the monad
laws, and that's how monads work.

(A quick primer - anything is a "monad" if it obeys some really simple
laws.  It has to be some wrapper class around a value (it's more
abstract than that, actually, but talking about wrappers is easy),
which exposes some function that takes a callback, typically called
"bind" or "flatMap".  Calling .bind() is very similar to calling
.map() on an array - .map() takes a function, and calls it for every
element in the array, making a new array with the return results.  The
only difference is that the callback to .bind() is expected to return
a value in the same monad, so you get double-wrapping.  For example,
if you called .map() and only returned arrays, you'd get an array of
arrays as the result.  The magic here that makes it a monad is that
the class knows how to "flatten" itself one level, so it can take the
return result of .bind(), which has a double-wrapped value, and turn
it into a single-wrapped value.  This is how .then() works on Promises
- if you return a Promise<b> from the callback, you'll get a
Promise<Promise<x>>, but the Promise class knows how to flatten itself
by one level, so it does so and hands you back a simple Promise<x>.

To help drive the concept home, say we did "[1, 2, 3].map(x=>[x,
10*x])".  The return result would be "[[1,10],[2,20],[3,30]]".  On the
other hand, if we defined "bind" (/flatMap/chain/then) on Array, we
could do the exact same thing "[1, 2, 3].bind(x=>[x, 10*x])", but the
return result would be "[1, 10, 2, 20, 3, 30]" - it automatically
flattens itself one level.  (It's not obvious in this example, but it
really does only do one level - if I'd returned [x, [10*x]] from the
callback, the return from .bind() would be "[1, [10], 2, [20], 3,
[30]]".))

~TJ


More information about the es-discuss mailing list