3

Assuming you have to chain function that return promises, where each function needs values returned by some (not necessarily the last) other promise.

Does this pattern have a name, and is it feasible cleanly with promises ?

E.g :

return getA().then(function (a) {

    getB(a).then(function (b) {

         getC(a, b).then (function (c) {
             return {
               a : a,
               b : b,
               c : c
              }
         });
    });
});

You can't "naively" flatten this :

getA().then(function (a) {
   return getB(a);
}).then(function (b) {
   // Obviously won't work since a is not there any more
   return getC(a, b); 
}).then(function (c) {
   // Same game, you can't access a or b
});

The only alternative I can see is that getA, getB and getC actually return a Promise resolved with an object containing a, then b, then c. (So each function would build it's part of the final result.)

getA().then (function (result) {
  return getB(result.a);
}).then(function (result) {
  return getC(result.a, result.b);
}).then(function (result) {
  // Last function is useless, just here to illustrate.
  return result;
});

Is there a way to flatten the code in my first example?

tcooc
  • 20,629
  • 3
  • 39
  • 57
phtrivier
  • 13,047
  • 6
  • 48
  • 79
  • `You can't "naively" flatten this :` yes you can. :) use .then to modify the results of getB and getC – Kevin B Aug 21 '14 at 17:17
  • @KevinB not sure I get your idea, could you add an example, please ? – phtrivier Aug 21 '14 at 17:18
  • Hmm, I've been trying to look at this objectively, but all the suggestions for improving your code are primarily opinion-based. I can say, however, your first example is not the correct approach to a promise chain. – tcooc Aug 21 '14 at 17:22

4 Answers4

4

The reason promises are used is to avoid nested callbacks. Your first example causes the promise chain to regress into a bunch of callback functions nested within each other. (Don't do that)

The chain can be used like this:

var first, second, third;
getA().then(function (a) {
   first = a;
   return getB(first);
}).then(function (b) {
   second = b;
   return getC(first, second); 
}).then(function (c) {
   third = c;
   return [first, second, third]; // have everything now!
});

You should probably take another look at your code, too. An ideal promise chain should look like this:

getA().then(getB).then(getC).then(function(result) {
  // done!
});

(Unless those functions are from a library, of course.)

tcooc
  • 20,629
  • 3
  • 39
  • 57
  • I suppose the promise experts will dislike this one because it doesn't use nifty promise features to solve the problem, but I like it because frankly it's the clearest and most obvious solution of any of these. Even a non-promise expert can glance at the code and see what it's doing. – jfriend00 Aug 22 '14 at 00:07
  • Yes, i should have clarified that, but the situation is usualy one where getA / getB / getC are from either from libraries or sufficiently general that you can not tailer their return values. – phtrivier Aug 22 '14 at 10:02
4

Here is stuff you don't actually need:

  • Additional closure variables
  • Any nesting. Or nesting Promise.all calls
  • Implementing complicated logic

The trick here is to use promises as proxies for values which would let you do this line after line just like you would with other sequential code. Here is "natively flattening" this:

var a = getA();
var b = a.then(getB);
var c = Promise.all([a,b]).spread(getC); // .all with a lambda if no .spread

Now let's say you want to access a b or c:

// .then and unwrap arguments if no spread, in Bluebird it's even better with join
Promise.all([a,b,c]).spread(function(a, b, c){
    //a b and c are available here, note how we didn't need to nest
});
Benjamin Gruenbaum
  • 270,886
  • 87
  • 504
  • 504
  • Is `.spread() a Bluebird-specific feature? Can this be done with standard ES6 promises built into Chrome/node? – jfriend00 Aug 22 '14 at 00:14
  • @jfriend00 yes, `.spread` is merely a shortcut for ES6 destructuring however that's not implemented in browsers yet so promise libraries implement `.spread`. All `.spread` does is `.then(r){ return fn.apply(null, r) })` after all so it's an easy shim. – Benjamin Gruenbaum Aug 22 '14 at 07:39
  • This is horrendous. The final consumer function isn't so bad but look at the awkwardness involved in creating `a`, `b` and particularly `c`! – Roamer-1888 Aug 22 '14 at 10:31
  • @Roamer-1888 I think that given OP's needs this is by far the simplest and most elegant approach and for what it's worth - with a solid promise library this is considerably easier. `c` would be `Promise.join(a, b, getC)`. You can of course use a regular `.then`. If you want to unwrap the value at each stage more elegantly - you can use generators, that's _exactly_ what they do when `yield` resolves promises only with nicer syntax. – Benjamin Gruenbaum Aug 22 '14 at 12:32
  • Hi Benjamin. Sorry, "horrendous" was unkind. Better phrased, you have maybe 99th percentile capability in this sort of thing. You are immersed in it, with Bluebird development and all sorts. I have no doubt that your solution is thoroughly correct and certainly concise, but most programmers wouldn't be able to compose `Promise.all([a,b]).spread(getC)` for themselves or even `Promise.join(a, b, getC)`. As the accepted answer, this would appear to satisfy the OP, but is it good generalised advice? Food for thought. – Roamer-1888 Aug 22 '14 at 15:25
  • @Roamer-1888 you most certainly don't have to apologize for calling it names. It's perfectly fine to voice your opinion about code and there is nothing unkind about it. Moreover what credentials I (or any other user) may or may not have are irrelevant when commenting on an answer. Personally I'd much rather get direct feedback and opinions over "political" polite responses. I do indeed believe that this is good advice by the way - this makes for cleaner code which is easier to reason about IMO and every variable is used as a proxy for its eventual value which is essentially what promises are. – Benjamin Gruenbaum Aug 22 '14 at 15:30
  • Thanks Benjamin, I learn a lot from your answers and hopefully this one will be no exception - when it sinks in. – Roamer-1888 Aug 22 '14 at 15:34
2

Yes, use the "accumulator pattern".

Write getA, getB and getC to accept an object, augment it, and return either it or a new promise resolved with it.

eg.:

function getA(obj) {
    // ...
    // read/augment obj
    // ...
    return obj; // or return Promise.resolve(obj);
}

Now, the calling expression can be an ultra-flat chain :

function foo() {
    var obj = {
        //initial properties
    };
    return Promise.resolve(obj).then(getA).then(getB).then(getC);
}

Thus obj is a token that is passed down the chain. Anything done to obj by any of the processor functions getA/getB/getC is observable by all processor functions that follow.

Clearly, all processor functions (except possibly the last in the chain) must be disciplined :

  • they must comply with the protocol (accept/augment/return an object)
  • they must not inappropriately overwrite or otherwise destroy properties of obj already present.

For this reason the "accumulator pattern" can't be directly used with just any old third-party functions, though adaptive wrappers for such functions should be simple to write.

DEMO

Roamer-1888
  • 19,138
  • 5
  • 33
  • 44
0

If your promise library supports .all() (or you're using the ES6 standard promises, which support .all()), you should be able to do something like this:

getA().then(function (a) {
   return Promise.all([a, getB(a)]);
}).then(function (ab) {
   return Promise.all(ab.concat(getC(ab[0], ab[1])));
}).then(function (abc) {
   return { a : abc[0], b : abc[1], c : abc[2] };
});

On the other hand, tcooc's suggestion of declaring variables in advance to hold the results is pretty straightforward and is definitely worth consideration.

JLRishe
  • 99,490
  • 19
  • 131
  • 169
  • I don't doubt this might work, but man is it messy for a fairly simple problem. The benchmark for readability/intent of operation seems to be tcooc's brute force, but obvious solution and this one seems less readable, less understandable and less maintainable than tcooc's answer. – jfriend00 Aug 22 '14 at 00:03