Comparison of two different approaches to structuring code that uses Promises.
Version using .then()
, from Complex task dependencies:
files.getLastTwoVersions(filename)
.then(function(items) {
return [versions.get(items.last),
versions.get(items.previous)];
})
.spread(function(v1, v2) {
return diffService.compare(v1.blob, v2.blob)
})
.then(function(diff) {
// voila, diff is ready. Do something with it.
});
With wrapped functions
items = files.getLastTwoVersions(Promise.resolve(filename));
v1 = versions.get(items.get("last"));
v2 = versions.get(items.get("previous"));
diff = diffService.compare(v1.get("blob"), v2.get("blob"));
diff.then(function (s) {
// voila, diff is ready. Do something with it
});
3rd Alternative (full source)
function property(name) {
return function(obj) { return obj[name]; }
}
items = files.getLastTwoVersions(filename);
v1 = files.then(property('first')).then(versions.get);
v2 = files.then(property('last')).then(versions.get);
diff = Promise.join(v1.then(property('blob')), v2.then(property('blob')))
.spread(diffService.compare);
diff.then(function(s) { /* voila */ });
In general, you want the inputs of your function to always be values and let then
and spread
do the promise unpacking. Its almost equally easy to write diffService.compare(v1.get("blob"), v2.get("blob"));
and Promise.join(v1.then(property('blob')), v2.then(property('blob'))).spread(diffService.compare);
, except the second one is more general.
Instead of writing separate functions that work with promises, write higher order functions that can be passed to then and spread. Note that these (such as the property function) do not unpack promises. Separation of concerns :)
With ES6 arrow functions defining higher order functions becomes quite elegant:
let property = name => obj => obj[name];
items = files.getLastTwoVersions(filename);
v1 = files.then(property('first')).then(versions.get);
v2 = files.then(property('last')).then(versions.get);
diff = Promise.join(v1.then(property('blob')), v2.then(property('blob')))
.spread(diffService.compare);
diff.then(s => /* voila */);
And perhaps quite unnecessary :)
items = files.getLastTwoVersions(filename);
v1 = files.then(items => versions.get(items.first));
v2 = files.then(items => versions.get(items.last));
diff = Promise.join(v1, v2).spread((v1, v2) =>
diffService.compare(v1.blob, v2.blob));
diff.then(s => /* voila */);
@spion I do prefer the 3rd version to the 1st, but I think the wrapped version has very much better readability in almost all cases.
For example something like
reads a lot better that a
.then()
equivalent, which would require you to manually track the intermediate values, and nest the.then()
calls. And even better,Promise.
,.then()
,.spread()
, etc. does not appear at all in the source code. (I don't think it a good idea to be able to use Promises without ever knowing that you're doing so, but it's helpful to declutter your code when you're puzzling out an algorithm.)Also, with wrapped functions you can also do normal-looking method calls. This does expose some of the implementation, but it is at least possible.
An example where string methods are called on a Promise that resolves to a String:
Array methods like
.reduce()
also work as you'd expect:T()
is defined: