5

I am making a series of http requests and I need to file the results into a list object when they return. I'm using angular promises.

Because the promises only resolve after the for loop is finished, they all get filed into the last index of list.

for (var i = 0;i < list.length; i+=1) {
    Promise.do(action).then(function(result) {
        list[i] //i is always at last index because the for loop has already completed
    }
}
Qantas 94 Heavy
  • 15,750
  • 31
  • 68
  • 83
Tom
  • 1,546
  • 2
  • 20
  • 42
  • 3
    possible duplicate of the [infamous loop issue](http://stackoverflow.com/q/1451009/1048572) in case you question is just about the `i` index in the callback, and not about how to get a promise for a list. Use closures! – Bergi Aug 15 '14 at 11:44
  • @Bergi: Technically the problem is due to the closure. Use functions to break closures! – slebetman Aug 15 '14 at 12:04
  • @slebetman: OK, technically: Use an IIFE to give the closure its own parent scope. (without closure, callbacks wouldn't work well) – Bergi Aug 15 '14 at 12:12
  • @Bergi: Callbacks would work perfectly well without closures. I should know - I've been using them in languages without closures for years before I encountered javascript. In fact, the pervalent setTimeout/setInterval design patterns don't need closures at all and we even used to use string callbacks (remember them) which didn't use closures at all and the web worked fine. First class functions and closures are different concepts. There are functional languages out there with first class functions but not closures. In fact the original Lisp didn't have closures (first introduced in Scheme) – slebetman Aug 15 '14 at 12:44
  • @slebetman: Sure callbacks alone work without closures, but then it's complicated (explicit, with more-or-less global variables) to access outer values like `i`. In purely functional languages, you'd probably use partial application (don't know Lisp that well, did you use homoiconicy for that?). – Bergi Aug 15 '14 at 13:22
  • @Bergi: I don't know much Lisp either apart from reading up SICP. I just know that closures were invented much later with Scheme. I used Tcl a lot and did some Forth hacking. Both very primitive but purely functional (Though tcl programs traditionally looks procedural, there are almost no syntax in tcl. Instead, language constructs were implemented as functions). And you guessed it, maintaining state used globals which is a much older concept to closures (though related). Tcl made it less painful with namespaces allowing you to have multiple global scopes. – slebetman Aug 15 '14 at 13:32
  • 1
    Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/59411/discussion-between-bergi-and-slebetman). – Bergi Aug 15 '14 at 13:33

5 Answers5

3

Bind the index as an argument of the function receiving the result:

for (var i = 0;i < list.length; i+=1) {
    Promise.do(action).then((function(index, result) {
        list[index]
    }).bind(null, i));
}
Volune
  • 4,324
  • 22
  • 23
2

Don't use a standard loop. Use Array.forEach instead. Every time the function supplied to forEach is called, you'll get a new closure and therefore a new copy of i

spender
  • 117,338
  • 33
  • 229
  • 351
1

I would try to use $q.all for this:

var promises = [];

for (var i = 0; i < list.length; i += 1) {
    promises.push(Promise.do(action));
}

$q.all(promises).then(function(results) {
    console.log(results);
});

From the documentation:

Returns a single promise that will be resolved with an array/hash of values, each value corresponding to the promise at the same index/key in the promises array/hash. If any of the promises is resolved with a rejection, this resulting promise will be rejected with the same rejection value.

dfsq
  • 191,768
  • 25
  • 236
  • 258
0

Agree with Bergi's comment that this looks like the infamous loop issue. However, I typed my answer before noticing Bergi's comment, so here are a couple of proposals for solution:

for (var i = 0;i < list.length; i+=1) {
    var callback = function(result) {
        list[arguments.callee.i]...
    }
    callback.i = i;
    Promise.do(action).then(callback);
}

OR:

for (var i = 0;i < list.length; i+=1) {
    var callback = function(n) {
        return function(result) {
            list[n]...
        };
    }(i);
    Promise.do(action).then(callback);
}
masa
  • 2,762
  • 3
  • 21
  • 32
-1

You can call the next round of the 'loop' within the Promise.then block, like this:

function do_thing(i) {
    // Are we finished? If so, return.
    if (i == list.length) return;
    Promise.do(action).then(function(result) {
        list[i]
        // Do the next thing, only once the previous thing completed.
        do_thing(i+1)
    }
}
// Start at the first thing.
do_thing(0)

The problem with this is that you lose the benefit of having parallel async calls happening - everything is serialised - so it will be slower. With a jQuery promise you could attach the index to the promise object... would this work in angular?

for (var i = 0;i < list.length; i+=1) {
    var p = Promise.do(action)
    p.index = i
    p.then(function(result) {
         // this.index is the value of i so do...
         list[this.index] 
    }
}

Here's a fiddle showing that in jQuery flavour: http://jsfiddle.net/sifriday/0v6vr77y/

sifriday
  • 4,342
  • 1
  • 13
  • 24