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??
In reply to Bizarre Results when Creating a Closure by liverpole
| For: | Use: | ||
| & | & | ||
| < | < | ||
| > | > | ||
| [ | [ | ||
| ] | ] |