Perl - Best practices when sending blocks to subs
I'm a big fan of functional programming, so when I discovered block references in Perl I started using them a lot.
However, the functions I've written that take blocks as arguments are written in this style:
sub mygrep (&@) {
my $code = shift;
my @result;
foreach $_ (@_) {
push(@result, $_) if &$code;
}
@result;
}
(From http://perldoc.perl.org/perlsub.html#Prototypes)
In essence, most of my functions set $_
in order for the code block to gain access to data in my开发者_如何学运维 sub. I guess my question can be split into three subquestions:
- Are there some major pitfalls in this approach?
- Is it a better idea to
local
ize$_
before setting it? - Should i use partially applied functions instead?
I'm still a Perl newbie so any answers and suggestions are appreciated - thanks in advance! :)
In the code you have written:
sub mygrep (&@) {
my $code = shift;
my @result;
foreach $_ (@_) {
push(@result, $_) if &$code;
}
@result;
}
The foreach
loop is implicitly localizing the $_
variable on every loop iteration. It is perfectly safe (and the fastest way to get values into $_
properly).
The only nit that I have with the code above is that every time &$code
is executed, it has access to the source argument list, which could cause a bug. You could rewrite the code as follows:
sub mygrep (&@) {
my $code = shift;
my @result;
foreach $_ (splice @_) {
push(@result, $_) if &$code; # @_ is empty here
}
@result;
}
Here are a few other ways you could write that function:
sub mygrep (&@) {
my ($code, @result) = shift;
&$code and push @result, $_ for splice @_;
@result
}
sub mygrep (&@) {
my $code = shift;
# or using grep in our new grep:
grep &$code, splice @_
}
Each of these examples provides an aliased $_
to its subroutine, with proper localization.
If you are interested in higher order functions, I'd encourage you to take a look at my module List::Gen on CPAN, which provides dozens of higher order functions for manipulating both real and lazy lists.
use List::Gen;
my $list = filter {$_ % 2} <1..>;
# as a lazy array:
say "@$list[0 .. 5]"; # 1 3 5 7 9 11
# as an object:
$list->map('**2')->drop(100)->say(5); # 40401 41209 42025 42849 43681
zip('.' => <a..>, <1..>)->say(5); # a1 b2 c3 d4 e5
How about using $code->($arg)
?
sub mygrep (&@) {
my $code = shift;
my @result;
foreach my $arg (@_) {
push(@result, $arg) if $code->( $arg);
}
@result;
}
I haven't tested it but I would assume this would work, and it would let you pass additional arguments to $code
.
Updated: this looked fun so I went ahead and tested it. It works just fine, see below (I intensely dislike prototypes, so I removed it, especially as it kept complaining about @a not being an array ref ;--(
#!/usr/bin/perl
use strict;
use warnings;
sub mygrep {
my $code = shift;
my @result;
foreach my $arg (@_) {
push(@result, $arg) if $code->( $arg);
}
@result;
}
my @a= ( 1, 2, 3, 4, 5, 6);
print mygrep( sub { return shift() % 2 }, @a), "\n";
And of course the main fun with this line of thinking is also to generate the code;
#!/usr/bin/perl
use strict;
use warnings;
sub mygrep {
my $code = shift;
my $filter= shift;
my @result;
foreach my $arg (@_) {
push(@result, $arg) if $code->( $arg);
}
@result;
}
my @a= ( 1, 2, 3, 4, 5, 6, 7, 8, 9);
print mygrep( mod_filter( 3), @a), "\n";
print mygrep( mod_filter( 4), @a), "\n";
sub mod_filter
{ my( $filter)= @_;
return sub { ! (shift() % $filter) };
}
1. Are there some major pitfalls in this approach?
my $_;
in view of the block will hide your changes to package variable$_
. There's nothing you can do about that from inside ofmygrep
.&$code
is very special. You want&$code()
or$code->()
instead.Changing
$_
will change the arguments passed tomygrep
. That's undesirable here.
2. Is it a better idea to localize $_ before setting it?
for
provides much better localisation that local
, but it also provides aliasing that's undesirable here.
3. Should i use partially applied functions instead?
I don't know what that means.
Fixed:
sub mygrep (&@) {
my $code = shift;
my @result;
for (@_) {
# Create copy so $_ can be modified safely.
for (my $s = $_) {
push @result, $_ if $code->();
}
}
return @result;
}
That said, I think mygrep
is kind pointless, since map
+grep
already does what you want more easily. Compare
mygrep { if ($_ % 2) { ++$_; 1 } else { 0 } } LIST
with
map { $_+1 } grep { $_ % 2 } LIST
You can even merge the map
and grep
.
map { $_ % 2 ? $_+1 : () } LIST
It's absolutely better to localize $_
. The subref can modify the value of $_
, and those changes will propagate into the calling function. This isn't a problem in the mygrep()
case, but could be in others.
精彩评论