Perl Syntax combining `foreach` and `if`: Shouldn't it work, i.e.: Why doesn't it work?

Viewed 201

Trying to get the maximum in an array that cannot contain negative numbers I tried:

my @v;
#...
my $max = 0;
$max = $_
    if ($_ > $max)
        foreach (@v);

I get a syntax error with perl 5.18.2.

However (1) statement($_) foreach (@v); and (2) $max = $_ if ($_ > $max); are both OK, and they do what they are supposed to do.

So if (1) and (2) are both valid statements, why can't they be combined in the pattern used for (1)?

Just that noone needs to suggest that; here's how I solved the problem using a different syntax:

foreach (@v) {
    $max = $_
        if ($_ > $max);
}
2 Answers

So if (1) and (2) are both valid statements, why can't they be combined in the pattern used for (1)?

The postfix syntax (aka statement modifier), which you are using here, just does not support this kind of statement.

The documentation says (emphasis mine):

Any simple statement may optionally be followed by a SINGLE modifier, just before the terminating semicolon (or block ending).

As for your task itself: getting the max value from an array is a very common requirement. A simple and standard approach uses function max() from core module List::Util:

use List::Util qw/max/;
my @v;
my $max = max 0, @v;

I will add this more generic answer, because wanting to combine for & if statement modifiers is pretty common, not only for trivial cases where you can use a (much faster) CPAN module. The reason why you might want to not do the standard foreach that uses a block, is that a block costs time, which might be insignificant if what happens inside the loop is slow, but might be over 10% of overhead if you are doing something like a simple assignment. e.g. this simple example:

my @v = map { rand } ( 1..100 );
cmpthese(-2, {
    postfix => sub {
        my $last;
        $last = $_ for @v;
    },
    block => sub {
        my $last;
        foreach (@v) {$last = $_}
    },
});

Gives me:

            Rate   block postfix
block   214369/s      --    -11%
postfix 241751/s     13%      --

So... while you cannot use two statement modifiers (postfix if, for), most of the time you can replace the for with a map, retaining the speed of a block-less loop. For example OP's example would be map {$max = $_ if $_ > $max} @v and benchmarked:

my @v = map { rand } ( 1..100 );
cmpthese(-2, {
    map => sub {
        my $max = 0;
        map {$max = $_ if $_ > $max} @v;
    },
    foreach => sub {
        my $max = 0;
        foreach (@v) {$max = $_ if $_ > $max}
    },
});

gives:

            Rate foreach     map
foreach 205585/s      --    -13%
map     236303/s     15%      --

As I said, in the specific case, you should definitely use List::Util which is written in C and will be an order of magnitude faster...

Related