liverpole has asked for the wisdom of the Perl Monks concerning the following question:

Greetings fellow monks,

This evening I was in the process of doing some work on a GUI I've been developing, when I came across a very strange phenomenon that I can't explain.  The test program here exhibits the same symptoms I found in the (much longer) original program.  I've kept it as a GUI, but I'm guessing it would be quite easy to duplicate this as a 10-15 line simple Perl script as well.

First of all, here's the program:

#!/usr/bin/perl -w # # 070502 liverpole -- Demonstrates a weirdness which I don't understa +nd # # Strict use strict; use warnings; # Libraries use Tk; ################## ## Main program ## ################## # Construct the GUI my $mw = new MainWindow(); my $f1 = $mw->Frame()->pack; my $chosen = ""; my $fr1 = $f1->Frame()->pack(-side => 'left'); my $fr2 = $f1->Frame()->pack(-side => 'left'); button($fr1, 'green', ' Exit GUI ', sub { $mw->destroy() }); button($fr1, 'green', ' Start test ', sub { start_test($fr2) }); $fr1->Label(-width => 16, -textvar => \$chosen, -bg => 'white')->pack; MainLoop(); ################# ## Subroutines ## ################# sub button { my ($w, $bg, $text, $pcmd) = @_; my $b = $w->Button(-bg => $bg, -text => $text); $b->configure(-width => 16, -command => $pcmd); $b->pack; return $b; } sub randomize_list { my $plist = shift; my $prandom = [ ]; while (@$plist) { push @$prandom, splice(@$plist, int(rand(@$plist)), 1); } return $prandom; } sub start_test { my ($frame) = @_; # Create a random list of 9 items { 1 ... 9 } my @values = qw( 1 2 3 4 5 6 7 8 9 ); my $pvalues = randomize_list([ @values ]); my $chosen_idx = -1; foreach my $num (@$pvalues) { $chosen = $num; # Create a duplicate list (not including the chosen number) my @dups = grep { $_ ne $num } @values; my $pdups = randomize_list([ @dups ]);; # Insert chosen number at a random place { 0 ... 3 } in the li +st my $correct_idx = int(rand(4)); $pdups->[$correct_idx] = $num; my $pbuttons = [ ]; for (my $i = 0; $i < 4; $i++) { my $dup = $pdups->[$i]; my $icopy = $i; # The closure where I'm expecting $chosen_idx to get # assigned to $i = {0,1,2,3}, but it seems to always # get assigned to 4! (or whatever the final value of # $i is.) However, if I set $chosen_idx to $icopy: # # my $psub = sub { $chosen_idx = $icopy }; # # then everything works ... ??? # my $psub = sub { $chosen_idx = $i }; push @$pbuttons, button($frame, 'skyblue', $dup, $psub); } $chosen_idx = -1; while ($chosen_idx < 0) { $mw->update(); } # Delete the buttons map { $_->destroy() } @$pbuttons; # Why on EARTH is this showing $chosen_idx as 4 every time?! print "correct idx[$correct_idx] ... chosen_idx[$chosen_idx]\n +"; } }

When I run it, it creates a small GUI where you click on the "Start test" button, and it displays a single random value from 1-9 in a Label widget on the left, and 4 buttons on the right (one of which contains the same value as in the Label).

Okay, so far so good.  But now, when you click on one of the 4 buttons, it prints to STDOUT what the correct index of the chosen value is (from 0 to 3), and the index of the button you pressed.  However, the second index is always a 4?!

I don't understand why this should be true.  I'm constructing a closure $psub around each value of $i in the loop, which can be verified by printing the value of $i in the button subroutine:

sub button { my ($w, $bg, $text, $pcmd, $ival) = @_; (defined $ival) and print "Value of \$i is $ival\n"; my $b = $w->Button(-bg => $bg, -text => $text); $b->configure(-width => 16, -command => $pcmd); $b->pack; return $b; } # ... and later, when calling button() ... push @$pbuttons, button($frame, 'skyblue', $dup, $psub, $i); # The above correctly displays each value of $i

I've also tried making $i global in case it was somehow related to its scope within the for statement, but that didn't change anything.

The workaround I'm now using is to make a copy of $i called $icopy, and assign to it instead:

my $icopy = $i; # ... my $psub = sub { $chosen_idx = $icopy };

And it's clearly somehow related to the final value of $i at the end of the for loop (to verify this, try changing the loop to for (my $i = 0; $i < 7; $i++) {, for example).

But, can some wise monk please explain to me why the closure is not working the way I'd expect it to, with respect to the interim values if $i??


s''(q.S:$/9=(T1';s;(..)(..);$..=substr+crypt($1,$2),2,3;eg;print$..$/

Replies are listed 'Best First'.
Re: Bizarre Results when Creating a Closure
by merlyn (Sage) on May 03, 2007 at 02:26 UTC
    You have indeed correctly identified the problem. When you create the closure:
    my $psub = sub { $chosen_idx = $i }
    It binds to the variable $i, not the current value. And since you change the variable later, the value changes too.

    If you want to bind it to the current value, you have to copy it, as you did in your $icopy example.

Re: Bizarre Results when Creating a Closure
by Errto (Vicar) on May 03, 2007 at 22:48 UTC
    sub something { my $i = whatever; ... sub { closure involving $i } }
    Each time this closure is evaluated it gets the version of $i from the current invocation of something, which is a different $i each time. But
    for my $i (@some_values) { ... sub { closure involving $i } }
    doesn't work the same way because each iteration you're getting the same variable $i with a different current value; it's not actually a different variable each time through.

    The fact that Perl lets you create closures around variables that can be subsequently altered is nice in some ways, but it can catch one off guard.

Re: Bizarre Results when Creating a Closure
by liverpole (Monsignor) on May 04, 2007 at 14:07 UTC
    Thanks to all who replied.

    I think merlyn may have said it best:  "It binds to the variable $i, not the current value."

    And of course, I knew that ... but somehow wasn't comprehending why my closure didn't DWIM, and just use the current value of $i each time through the loop.

    So now I'll just remember the rule:  "a closure binds to the variable, NOT to its value", and I should be all set.


    s''(q.S:$/9=(T1';s;(..)(..);$..=substr+crypt($1,$2),2,3;eg;print$..$/
Re: Bizarre Results when Creating a Closure
by thundergnat (Deacon) on May 03, 2007 at 18:47 UTC

    You can do the closure without needing the explicit $icopy temp variable. Just write the code reference line like:

    my $psub = [ sub { $chosen_idx = $_[0] }, $i ];

      That produces a reference to a list, not a code ref like the original. Instead of calling $psub->(), you have to write $psub->[0]->($psub->[1]) instead, and that works, but it sure is ugly.

        True, and perhaps I should have been a little clearer...

        The coderef in question is being used as a callback for a PerlTk widget, and a documented way to do closures for widget callbacks is to pass a reference to a list consisting of a coderef an anonymous sub as the first parameter followed by arguments.

        Reference "perldoc Tk::callbacks" or (Google) Tk::callbacks site:cpan.org

        Did you try actually running the code with the modification I proposed? It works exactly as expected.