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()
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)
})
}
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)
})
}
//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)
})
})
}
//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) })
})
})
}
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?
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.
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.
I think those examples are over complicated.
Assuming you have a transformer (
read
in this case) which is implemented in terms of promise it also would be a lot easierif 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 (withfunction(err, result)
callback) into promisified functions could be done automatically.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.