Skip to content

Instantly share code, notes, and snippets.

@Raynos
Last active December 17, 2015 19:19
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 Raynos/75084680af5a2862c99b to your computer and use it in GitHub Desktop.
Save Raynos/75084680af5a2862c99b to your computer and use it in GitHub Desktop.
Promises are complex

Promises are complex.

Why are promises complex? Because they complect a lot of semantics into a single operation.

define: complect

be interwoven or interconnected; "The bones are interconnected via the muscle".

Complection means that many different operations are interwoven and interconnected into a single thing.

In the case of promises, this single thing is .then()

Value transformation.

Say you want to transform the value within a promise. This should be a simple operation.

Let's say we have a promise for a HttpResponse and we want to create a promise for the body.

var body = map(response, function (response) {
  return response.body
})

map is very simple to implement.

function map(promise, lambda) {
  return new Promise(function (resolve, reject) {
    promise.then(function (x) { resolve(lambda(x)) }, reject)
  })
}

Asynchronous transformation

Say you want to asynchronously transform the value within a promise.

This is also a simple operation. Let's say we want to stat a file and then read it.

var file = chain(stat(file), function (stat) {
  return read(file)
})

chain is very simple to implement.

function chain(promise, lambda) {
  return new Promise(function (resolve, reject) {
    promise.then(function (value) {
      lambda(value).then(resolve, reject)
    }, reject)
  })
}

Asynchronous error handling

//TODO

function either(promise, recover, lambda) {
  return new Promise(function (resolve, reject) {
    promise.then(function (value) {
      lambda ? lambda(value).then(resolve, reject) : resolve(value)
    }, function (error) {
      recover(error).then(resolve, reject)
    })
  })
}

Shared computation

//TODO

function cache(promise) {
  var cached, resolves, rejects

  return new Promise(function (resolve, reject) {
    if (cached) {
      return (cached.v ? resolve(cached.v) : reject(cached.e))
    } else if (resolves) {
      return resolves.push(resolve), rejects.push(reject)
    }

    resolves = [resolve], rejects = [reject]
    promise.then(function (value) {
      cached = { v: value }
      resolves.forEach(function (r) { r(value) })
    }, function (error) {
      cached = { e: error }
      rejects.forEach(function (r) { r(error) })
    })
  })
}

Then is complected

In the recommended usage pattern for interacting with promises you just use then() for all four of these use cases. A single method overloaded to support all of them.

Each one of these operations is very easy to write (<10 loc). Yet for some reason the popular approach is to complect them into a single operation?

If we did not have a complex then what would a minimal promise be?

function Promise(handler) {
  return { then: function (f, r) { handler(f, r) } }
}

Combined with the primitives for sync (map) / async transformation (chain), error handling (either) and shared computation (cache). We can have a full promise implementation in a mere 50 lines.

This is also illustrates that the greatest amount of complexity lies in shared computation because it actually has to deal with shared state and that is complex.

Progress

To make things worse, there is talk of extending the already incredibly complex and complected operation of .then() to incorporate progress events, which means it should be able to handle "streaming" use-cases.

Streams are incredibly complex in their own right. The union of { map, chain, either, cache } and all stream operations in a single .then() method sounds pretty crazy.

@andreypopp
Copy link

I think those examples are over complicated.

Let's say we have a promise for a HttpResponse and we want to create a promise for the body.

bodyPromise = responsePromise
  .then(function(response) { return response.body; });

Say you want to asynchronously transform the value within a promise.

Assuming you have a transformer (read in this case) which is implemented in terms of promise it also would be a lot easier

file = stat(filename)
  .then(function(stat) { return read(file); });

if you don't have read implemented in terms of promises — you should do that, promises made to compose well with each other. Fortunately convertation of Node.js style async functions (with function(err, result) callback) into promisified functions could be done automatically.

Shared computation

I think it would be a lot easier to store promises in cache and update them in-place with a new promise when you need to invalidate a cache record, see example in connect-browserify (Sorry for CoffeeScript, you can look at the compiled version in the repo near).

Promises already cache the computed value.

@andreypopp
Copy link

oops, sorry regarding

Shared computation

you would not need cached at all cause promise already caches computed value or failure.

@jkroso
Copy link

jkroso commented May 28, 2013

then isn't really part of what a promise is, its an abstraction on top of promises, it comes bundled with most implementations but is still a separate concept. Its job is to connect computations. thats all so I don't see it as complected. The promise is the thing that represents the computation. Its a proxy. A pure promise would have three methods read, write, and error which pretty much do what they say. then should be built on top of read instead most implementations only offer then because it happens to make an OK read. I think it kind of sucks too that all promise specs define themselves in terms of then since when it comes time to inter-oping with each other they all treat then as read anyway. lol

My definitions:

  • promise: a proxy used to represent the result of a computation
  • then: connects computations

@andreypopp
Copy link

@jkroso

then isn't really part of what a promise is, its an abstraction on top of promises

I see then() being a part of Promises/A+ standard and, frankly, I don't see the reason to consider a promise implementation without then() — without it promise would be just a container with no much usefulness than storing data in a variable.

@robotlolita
Copy link

@andreypopp & @jkroso, I believe Raynos is talking about the complexity of Promises/A+ in particular, not the Promise-as-concept. Promises/A+ then() method is overtly complected and makes reasoning about anything more difficult — as you can see from part of the implementation here: https://gist.github.com/killdream/5306738

@andreypopp
Copy link

@killdream I just wanted to point that examples in the article are overcomplicated and I don't see how they undercover complexion (am I correct?) of then().

Now regarding complexion of then(). I don't see how semantics of then() is more complex than semantics of semicolon in programming language — it's simply a sequential execution. Yes it has some quirks which are because of limits of underlying platform and some opinions about autowrapping non-promise values in promises. But the main point is if you think of promises' then() as a semicolon for promise-resulting computation then how then() works is pretty transparent. But of course if you are going to talk about promises from the point of view of other abstractions then it will seem more complex.

@jkroso
Copy link

jkroso commented May 31, 2013

@killdream I don't really understand what your example is showing?

Id also like to try clarify my previous comment with an analogy of programming to film making. Films are made up of frames sequenced together on a roll of tape. A computation, A.K.A function call, is like a frame. And then connects computations so is like the films tape.

Of cause both are important but definitely separate. I guess its probably best to ignore the specs if your actually trying to understand this stuff.

@00dani
Copy link

00dani commented Jul 30, 2013

Honestly it's not even that complicated to implement Promises/A+-compliant promises – here's one compliant implementation in 52 SLOC, and I didn't even try that hard to shorten it!

It might give you a somewhat shorter, simpler implementation if you separate the fundamental control flow operations that then encapsulates, but I don't think it's valuable to do so; Promises/A+ gives you certain important guarantees about the nature of a promise that aren't as assured with these separate operations.

I think the most critical issue is that shared state is no longer a guaranteed aspect of all promises in this implementation. Since you need to transform promises to cached variants explicitly, there's no assurance that it's safe to then some arbitrary promise twice.

var promise = asyncCall();
promise.then(firstThing);
promise.then(secondThing);
// this is analogous to this synchronous code:
var value = syncCall();
firstThing(value);
secondThing(value);

Promises/A+ makes an explicit guarantee that the asynchronous code above, by analogy to the similar sync code, will work correctly. The separate-functions design provides no such guarantee, since you have no way of knowing whether a given promise has been cached.

The separation of map and flatMap (or bind or >>= or, as you've called it, chain) is less of a problem, and in fact to an extent I agree with that particular change; however, in practice implementing then as map-that-might-also-flatten works extremely well intuitively, and it's not really any more difficult to implement than separate map and flatMap.

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