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.
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 methodsread
,write
, anderror
which pretty much do what they say.then
should be built on top ofread
instead most implementations only offerthen
because it happens to make an OKread
. I think it kind of sucks too that all promise specs define themselves in terms ofthen
since when it comes time to inter-oping with each other they all treatthen
asread
anyway. lolMy definitions:
then
: connects computations