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.
@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 ofthen()
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 howthen()
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.