I have a question regarding code blocks in perl. Given the following code:
my @newArr = sort { $a <=> $b } @oldArr;
uses a code block as an argument.
I could rewrite it as:
sub sortFunc {
return $a <=> $b;
}
my @newArr = sort sortFunc @oldArr;
I am trying to figure out how this mechanism works. Currently I need to implement a sort of complex sort function that would look messy in a code block, However it depends on some local variables. For example:
foreach my $val (@values){
my @newArr = sort { $hash{$a}{$val}<=> $hash{$b}{$val} } @oldArr;
...
}
but lets assume that the sort function is more complex, so it wont neatly fit into the code above.
If I try to use a function(defined locally in the scope of the for loop) , I keep getting "Use of uninitialized value in hash element".
I assume that's because the sub is parsed once, and not recreated for evry iteration of the for loop. I am trying to understand how to implement a code block that would be reinterpreted every iteration, or maybe how to pass parameters
You didn't show the problematic code for some reason, but I think it's something like
for my $val (@values) {
sub sort_func {
return $hash{$a}{$val} <=> $hash{$b}{$val};
}
my @newArr = sort sort_func @oldArr;
}
I am trying to figure out how this mechanism works. [...] I assume that's because the sub is parsed once, and not recreated for evry iteration of the for loop.
Not quite. The following only parses and compiles the sub once, yet it works:
for my $val (@values) {
my $cmp_func = sub {
return $hash{$a}{$val} <=> $hash{$b}{$val};
};
my @newArr = sort $cmp_func @oldArr;
}
What matters is when what $val
is captured. $val
is captured when sub { ... }
is evaluated. Keeping in mind that
sub foo { ... }
is the same as the following in this regard,
BEGIN { *foo = sub { ... }; }
In my code, it captures the $val
from the foreach loop. In yours, it captures at compile time, so it captures the $val
that existed at compile time. And that's not the variable you want.
Now that you know how to make it work, you can move the complex code out of the way (out of the loop) as you desire.
sub make_cmp_func {
my ($hash, $val) = @_;
return sub {
return $hash->{$a}{$val} <=> $hash{$b}{$val};
};
}
for my $val (@values) {
my $cmp_func = make_cmp_func(\%hash, $val);
my @newArr = sort $cmp_func @oldArr;
}
Alternatively, you can pass the necessary values to the compare function instead of capturing them.
sub cmp_func {
my ($hash, $val, $a, $b) = @_;
return $hash->{$a}{$val} <=> $hash{$b}{$val};
}
for my $val (@values) {
my @newArr = sort { cmp_func(\%hash, $val, $a, $b) } @oldArr;
}
You want to use a function that takes arguments in addition to $a
and $b
.
sub my_sort_func {
my ($val, $a, $b) = @_;
return $hash{$a}{$val} <=> $hash{$b}{$val};
}
foreach my $val (@values) {
my @newArr = sort { my_sort_func($val,$a,$b) } @oldArr;
...
}
Perl's mechanism for using a code block with sort
is somewhat special, and not easily replicated in pure Perl.
Extending on mob's answer, this is one of the "clever but not necessarily smart" variety. If you object to the extra parameter, you can use currying instead.
sub make_sorter {
my ($hashref, $val) = @_;
return sub {
$hashref->{$a}{$val} <=> $hashref->{$b}{$val}
};
}
for my $val (@values) {
my $sorter = make_sorter(\%hash, $val);
my @newArr = sort $sorter @oldArr;
}
This isn't more efficient, more readable, or more valuable in really any way, but it might be interesting to know about the technique for someplace where it's actually useful.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With