Is there a pure Promise-based approach for mapping/concatenating collections?

Steve Perkins picture Steve Perkins · Aug 2, 2013 · Viewed 10.3k times · Source

async vs. Q generally

I'm learning Node.js development, and trying to wrap my brain around strategies for managing asynchronous "callback hell". The two main strategies I've explored are Caolan McMahon's async module, and Kris Kowal's promise-based Q module.

Like many other people, I'm still struggling to understand when you should use one vs. the other. However, generally speaking I have found promises and Q-based code to be slightly more intuitive, so I have been moving in that direction.

Mapping/Concatenating collections generally

However, I'm still stuck using the async module's functions for managing collections. Coming from a Java and Python background, most of the time when I work with a collection, the logic looks like this:

  1. Initialize a new empty collection, in which to store results.
  2. Perform a for-each loop with the old collection, applying some logic to each element and pushing its result into the new empty collection.
  3. When the for-each loop ends, proceed to use the new collection.

In client-side JavaScript, I've grown accustomed to using jQuery's map() function... passing in that step #2 logic, and getting the step #3 result as a return value. Feels like the same basic approach.

Mapping/Concatenating collections with async and Q

The Node-side async module has similar map and concat functions, but they don't return the concatenated result back at the original scope level. You must instead descend into the callback hell to use the result. Example:

var deferred = Q.defer();

...

var entries = [???]; // some array of objects with "id" attributes

async.concat(entries, function (entry, callback) {
    callback(null, entry.id);
}, function (err, ids) {
    // We now have the "ids" array, holding the "id" attributes of all items in the "entries" array.
    ...
    // Optionaly, perhaps do some sorting or other post-processing on "ids".
    ...
    deferred.resolve(ids);
});

...

return deferred.promise;

Since my other functions are becoming promise-based, I have this code returning a promise object so it can be easily included in a then() chain.

Do I really need both?

The ultimate question that I'm struggling to articulate is: do I really need both async and Q in the code example above? I'm learning how to replace the async module's control flow with Q-style promise chains generally... but it hasn't yet "clicked" for me how to do mapping or concatenation of collections with a promise-based approach. Alternatively, I'd like to understand why you can't, or why it's not a good idea.

If async and Q are meant to work together as I am using them in the example above, then so be it. But I would prefer not to require the extra library dependency if I could cleanly use Q alone.

(Sorry if I'm missing something outrageously obvious. The asynchronous event-driven model is a very different world, and my head is still swimming.)

Answer

Bergi picture Bergi · Aug 2, 2013

Do I really need both?

No. Mapping asynchronous iterators over a collection is quite simple with promises, but it requires two steps instead of one function call. First, the collection is mapped to an array of promises for the parallel iteration. Then, those promises are fed into Q.all to make one promise for the mapped collection. In contrast to async, the order of the result is guaranteed.

var entries = […]; // some array of objects with "id" attributes

var promises = entries.map(function(object) {
    return asyncPromiseReturingFunction(object);
}); // the anonymous wrapper might be omitted
return Q.all(promises);

For concat, you would have to append a

.then(function(results) {
     return Array.prototype.concat.apply([], results);
});