Given this Perl/Tkx code fragment:
@itemList = ({'attrib1' => 'name1', 'attrib2' => 'value1'},
{'attrib1' => 'name2', 'attrib2' => 'value2'});
$row = 0;
foreach $item (@itemList) {
push(@btn_list, new_ttk__button(-text => $item->{'attrib1'}, -command => sub {do_something($item->{'attrib2'});}));
$btn_list[-1]->g_grid(-column => 0, -row => $row);
$row++;
}
(In the real program @itemList is populated from a user editable config file.)
I do see two buttons labeled 'name1' and 'name2'. But when I click on either button it seems that the parameter that is passed to the callback is always $itemList[1]->{'attrib2'}
; i.e. 'attrib2' of the last element of the @itemList array. What I would like is to have the first button call do_something($itemList[0]->{'attrib2'}
and the second call do_something($itemList[1]->{'attrib2'}
.
What am I doing wrong, please and thank you?
You have encountered a subtle feature of for
loops in Perl. First the solution: use my
in the for loop. Then $item
will be able to create a proper closure in the anonymous sub you declare later in the loop.
for my $item (@itemlist) {
push(@btn_list, new_ttk__button(
-text => $item->{'attrib1'},
-command => sub {do_something($item->{'attrib2'});}));
$btn_list[-1]->g_grid(-column => 0, -row => $row);
$row++;
}
Further explanation: Perl implicitly localizes the subject variable of a for loop. If you don't use my
in the for loop, the loop will be using a localized version of a package variable. That makes your code equivalent to:
package main;
$main::item = undef;
@itemList = ({'attrib1' => 'name1', 'attrib2' => 'value1'},
{'attrib1' => 'name2', 'attrib2' => 'value2'});
$row = 0;
foreach (@itemList) {
local $main::item = $_;
push(@btn_list, new_ttk__button(
-text => $main::item->{'attrib1'},
-command => sub {do_something($main::item->{'attrib2'});}));
$btn_list[-1]->g_grid(-column => 0, -row => $row);
$row++;
}
# at the end of the loop, value of $main::item restored to undef
Your anonymous subs still refer to the $main::item
package variable, whatever value that variable holds at the time that those subroutines are invoked, which is probably undef
.
Shorter solution: use strict
Additional proof-of-concept. Try to guess what the following program outputs:
@foo = ( { foo => 'abc', bar => 123 },
{ foo => 'def', bar => 456 } );
my @fn;
foreach $foo (@foo) {
push @fn, sub { "42" . $foo->{bar} . "\n" };
}
foreach my $foo (@foo) {
push @fn, sub { "19" . $foo->{foo} . "\n" };
}
print $_->() for @fn;
Here's the answer:
42
42
19abc
19def