开发者

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:

  1. Are there some major pitfalls in this approach?
  2. Is it a better idea to localize $_ before setting it?
  3. 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?
  1. my $_; in view of the block will hide your changes to package variable $_. There's nothing you can do about that from inside of mygrep.

  2. &$code is very special. You want &$code() or $code->() instead.

  3. Changing $_ will change the arguments passed to mygrep. 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.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜