Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ForbesLindesay/5392701 to your computer and use it in GitHub Desktop.
Save ForbesLindesay/5392701 to your computer and use it in GitHub Desktop.

Arguments in favor of Promises for Promises

The argument in favor of Promises for Promises is structured as follows:

  1. Examples
  2. Lazy Promises
  3. Remote Promises
  4. Partial Results
  5. Error Handling
  6. Testing
  7. Abstract Theory
  8. The Identity Law
  9. Parametricity

Examples

Lazy Promises

It has occasionally proved useful to have promises which are lazy. That is to say they don't actually do the work until someone asks for the value via .then. These are useful in two scenarios:

  1. Providing a fluent API:

e.g. superagent (superagent's actual API is not promise based):

request.post('/api/pet')
       .send({ name: 'Manny', species: 'cat' })
       .set('Accept', 'application/json')
       .then(function(res){
         //Use result
       }, function (err) {
         //Handle error
       });

In this instance, you could return a partially configured request from an asynchronous method, and the caller could finish configuring it and then resolve the request. They can't do that if it's always already resolved.

  1. When you don't know if a value will actually ever be used and it's expensive to compute.

If promises are always flattened, you lose the ability to maintain this lazyness across multiple chained .then calls. You essentially lose some control over when the promises are evaluated.

Remote Promises

Remote promises are a similar problem to lazy promises. The idea here is that you have a "promise" for a remote object, such as a database. You can call methods on it (that return promises) but you can't get the object itself. These remote promises may resolve with themselves when you call then on them. They could equally well just not have a .then method though.

Partial Results

A nice neat example is where a function returns something for which partial results may be useful, and may be available much faster than the whole results. Lets assume for the example that promise can create a promise for a promise and promise.mixin(obj, fn) mixes the keys of obj into the promise.

This code is a get(url) funciton that makes an http request and returns a promise for a promise:

var request = require('hyperquest');
var concat = require('concat-stream');

function get(url) {
  return promise(function (fulfill, reject) {
    var responseStream = request(url, function (err, response) {
      if (err) return reject(err);
      fulfill(obj.mixin(response, function (fulfill, reject) {
        responseStream
          .pipe(concat(function (err, body) {
            if (err) return reject(err);
            fulfill(body);
          }))
      });
    });
  })
}

Example usage:

get('http://example.com/foo.json')
  .then(function (response) {
    if (response.statusCode != 200) {
      throw new Error('The server responded with status code ' + response.statusCode);
    }
    return response;
  })
  .then(function (body) {
    return JSON.parse(body);
  });

Error Handling

If you have a promise for a promise, and you know which part of a multi-stage operation each promise represents, you may be able to do better error handling because you can know more precisely where the error was thrown. It could be argued that this is better served by branding the errors (e.g. node.js ex.code === 'ENOENT').

Testing

I'm including testing here because it's often repeated as an example of why you would swap one monad for another. This doesn't necessitate nested promises, but I want to explain why actually it's not useful at all.

The idea is that in your unit tests you mock out functions that normally return a promise by returning an identity monad. This identity monad is then deterministic and synchronous. There are two reasons why this is misguided:

  1. The added determinism is a myth, most promise libraries are actually very close to determenistic with already resolved promises, and it's more useful to have tests that occasionally exhibit the pathalogical error cases, but be hard to reproduce, than have your tests never generate those error cases.
  2. Promises are always asynchronous, so it's safe to code with that assumption. It's dangerous to code with the assumption that they're always synchronous. If your tests are synchronous but your runtime is asynchronous you can assume neither and may accidentally assume that they are synchronous.

To conclude, it's never a good idea to swap a promise for an identity monad in unit test, just use a fulfilled promise.

Abstract Theory

The Identity Law

Consider a method Promise.of(a) that returns a promise for a. It might be implemented as:

Promise.of = function (a) {
  return new Promise(resolve => resolve.fulfill(a));
};

It is intuitively desirable for the left identity law to hold, it states that the following two things are equivallent:

Promise.of(a).then(f)
f(a)

providing f returns a promise and is deterministic.

This will always be true if Promise.of is allowed to return a promise for a promise, but not if it must recursively flatten its argument.

Parametricity

Parametricity is an idea from typed worlds. The concept is that you build a type out of other types. A good example is a list in C# or Java. Lists in those languages are generics, you can build a list of any type of object and they just take that object as a parameter. You aren't allowed to call any methods on the inner objects, becuase you don't know anything about their type.

Applying that same concept to an untyped world: if you inspect the contents of a promise in any way, and change your behavior based on those contents, you're no longer fully parameterised. This would mean that promises wouldn't be as generic as they could be, and when you deal with them, it's not as easy to reason about their behavior.

@myrne
Copy link

myrne commented Apr 16, 2013

@jden @ForbesLindesay I'm sorry. I've been out of line here. I'll leave it up to @ForbesLindesay . If he wants so, I'll remove my comments to reduce noise. Same applies for the other gist.

@erights
Copy link

erights commented Apr 17, 2013

If promises are always flattened, it is impossible to maintain this lazyness.

Q.makeRemote, defined at https://code.google.com/p/es-lab/source/browse/trunk/src/ses/makeQ.js#468 , modeled on http://wiki.erights.org/wiki/Proxy , is an existence proof that laziness is possible in a secure promise system that never allows a promise-for-promise. This is not a pleasant API for laziness nor is it meant to be. I offer it only as an existence proof, since it enables laziness to be expressed.

@briancavalier
Copy link

Promises for remotes may qualify here. A promise for a remote can provide itself (or an equivalent promise? I'll defer to @erights here) as its fulfillment value.

@rektide
Copy link

rektide commented Jun 6, 2013

Pro: a resolved promise-for-promise may have other slots besides .then and we may want to see those additional items. If we flatten promises, we may be skipping additional state they have.

@rektide
Copy link

rektide commented Jun 6, 2013

Pro promises-for-promises:

cost of implementing other spec if promises-for-promises allowed: very very very low.
cost of implementing flattening spec if promises-for-promises selected: impossible.

Given how easy it is to make flattening a library function, (a .thenAtLast) and given it's heavily heavily destructive nature, this is one of those golden cases where doing less, where not forcing everyone into some janky ass choice is the dumb, obvious & correct choice.

@rektide
Copy link

rektide commented Jun 6, 2013

Pro, in the abstract- control-flow representation:

In the abstract camp, I consider a promise as a control-flow construct. Put simply, a promise is a value for the output of a process. Sometimes that output is value signing the output of yet another process (a promise-for-promise).

That there is perhaps maybe data being distilled through these processes is somewhat ancillary; that data-flow perspective is important only if it happens to be the data that is important to you. This is a particular view some developers may adopt when using promises, but it is a subset, a more limited perspective of what a promise may be good for than a control-flow notion: it ignores the processes and skips straight to then, thinking that is the only value of promises.

@abuseofnotation
Copy link

I don't understand the 'Remote Promises' argument. What does the remote object from the example have to do with promises? To me it seems like a general object.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment