Foxed by the 'juggling async' learnyounode lesson

528 views
Skip to first unread message

tomg...@gmail.com

unread,
Jan 18, 2015, 9:44:24 AM1/18/15
to nod...@googlegroups.com
Hi there... I'm just going through the learnyounode exercises (so obviously a complete node.js beginner!) - and was proceeding quite confidently until I hit upon the 'juggling async' exercise. I have to admit tearing my hair out here and eventually cheating to get the 'official answer'. More frustrating was how close I was... and I am still not sure why mine does not work. I apologise if I am being stupid (very likely) - but I think it would really help my understanding to get to the bottom of this.
Anyway, what I did was this:

var http = require('http');
var bl = require('bl');

var htmls = [];

var count = 0;

for( var i = 0; i<3; i++){
   
    http.get( process.argv[2+i], function( r ){
        r.pipe(bl(function( err, buf ){
            count++;
            htmls[ i ] = buf.toString();
            if ( count == 3 ){
                print_results();
            }
        }))
    });
}


function print_results(){
    for( var j=0; j<3; j++ ){    
        console.log( htmls[j] );
    }
}


I then run e.g. 
node ex9.js http://www.google.com http://www.yahoo.com http://www.reuters.com

and get:
undefined
undefined
undefined



To get the correct answer, all I need to do is stick the stuff in the first 'for' loop in a function and instead loop through calling the function:

for( var i = 0; i<3; i++){ get_http(i) }

function get_http(i){
    http.get( process.argv[2+i], function( r ){
        r.pipe(bl(function( err, buf ){
            count++;
            htmls[ i ] = buf.toString();
            if ( count == 3 ){
                print_results();
            }
        }))
    });
}

Now it works! But why? I can see that 'i' is scoped differently in the second (correct) arrangement, but still don't see how it is it produces different results.
Very much appreciate any light that could be shed on this (for a complete beginner!)
Thanks :)



Adrien Risser

unread,
Jan 18, 2015, 2:48:13 PM1/18/15
to nod...@googlegroups.com
By the end of your first for loop, i value is already 3.

This is a beginner problem and is usually resolved using a closure like you did.


--
Job board: http://jobs.nodejs.org/
New group rules: https://gist.github.com/othiym23/9886289#file-moderation-policy-md
Old group rules: https://github.com/joyent/node/wiki/Mailing-List-Posting-Guidelines
---
You received this message because you are subscribed to the Google Groups "nodejs" group.
To unsubscribe from this group and stop receiving emails from it, send an email to nodejs+un...@googlegroups.com.
To post to this group, send email to nod...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/nodejs/c9baf277-19bc-4d83-8b21-045dc1102079%40googlegroups.com.
For more options, visit https://groups.google.com/d/optout.



--
Adrien Risser,
Freelance Node.js Consultant

Alexander Praetorius

unread,
Jan 18, 2015, 2:48:23 PM1/18/15
to nod...@googlegroups.com
actually i think it has to do with the index variable "var i" of the for loop and that by putting the body of the loop into a function "get_http", where the value of "i" in each execution of the loop body is "kept alive" by creating a seperate scope for each execution of the body, the the anonymous callback "function (r)..." body will use the value of "i" that existed during each execution of the for loops body.
If u do not create that scope, every anonymous callback will use the value of "i" that existed after the loop finishes, which is the "max i" value (i<3)...


tomg...@gmail.com

unread,
Jan 18, 2015, 10:25:50 PM1/18/15
to nod...@googlegroups.com
Ah yes! Thank you serapath, I figured it out from what you said (and I really was being stupid!) To clarify for anyone reading:

the loop kicks off the callbacks, but they don't actually get executed until the loop has finished. By that point i is 3, and because it is effectively globally scoped, this is the value that is used by *all* of the callbacks in the loop.
The solution is to create a local scope for i.

Thanks for the feedback guys, and sorry again for the dumb question.

By the way, is 'a local scope for i' the same as 'a closure' ? I was under the impression these two things were not the same...?

Peter Rust

unread,
Jan 19, 2015, 9:48:16 AM1/19/15
to nod...@googlegroups.com
> is 'a local scope for i' the same as 'a closure' ?

No. As the article on closures that Adrien linked to states, a closure is a function that refers to variables in the environment in which it is created.

So a closure in a loop looks like this:

for (var i = 0; i < 10; i++) {
  setTimeout(function() {
    console.log(i);
  }, 1000);
}

The inner function is called a closure because it refers to the variable "i", which is defined in the outer environment. The important thing to remember is that when the closure, the inner function, is created, it isn't binding to the current value of "i", but rather to the variable. The current value may be different if the function is executed later, as it is in this example, which will execute the 10 instances of the inner function after 1 second and all of them will see "i" as having a value of 10 because that's the state the loop left "i" in after it finished looping a second earlier.

But if you do something like this:

[0, 1, 2, 3, 4, 5, 6, 7, 8, 9].forEach(function(i) {
  setTimeout(function() {
    console.log(i);
  });
});

Note that there are two functions being created. The inner function is a closure because it refers to "i" which is coming from its environment, but the outer function is not a closure because the variable "i" is not defined in the outside environment, it is defined inside the function (in the arguments list). This is the function that gives "i" a local scope. Note that variables defined in the arguments list are treated the same as variables defined with "var" statements within the function -- they "belong" to that function, their scope is the function.

If you still want to use "for" instead of Array.forEach() (or if you want to use "for...in" instead of Object.keys()) then you can explicitly bind the current value of "i" to the inner function via Function.bind() and it will be passed into the function as an argument when the function is executed. This is called "currying". It isn't the primary function of bind (the primary function is binding the "this" value, but since we don't care about that, we pass "null" as the first argument to bind()):

for (var i = 0; i < 10; i++) {
  setTimeout(function(j) {
    console.log(j);
  }.bind(null, i), 1000);
}

We could have named the argument "i" as well, since this variable is scoped to the inner function, it will take precedence over the "i" in the outer function and would make the "i" in the outer function inaccessible. This function is not a closure because it isn't referring to a variable in the outside environment (but Function.bind() isn't magic, it probably uses a closure internally).

-- Peter Rust
Cornerstone Systems
Reply all
Reply to author
Forward
0 new messages