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.
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
cache
d variants explicitly, there's no assurance that it's safe tothen
some arbitrary promise twice.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
cache
d.The separation of
map
andflatMap
(orbind
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 implementingthen
asmap
-that-might-also-flatten works extremely well intuitively, and it's not really any more difficult to implement than separatemap
andflatMap
.