Search code examples
perl

Use of parentheses with a block argument in grep produces unexpected outcome


The first print of the following code does not print the expected result although the subroutine is passed as the block argument. It is expected to print 1 but print's 2. What is the difference between the first and second print statements?

my @arr = ("hello", "world");
print scalar(grep (sub {return "hello" eq $_}, @arr)); # return 2, not expected

print scalar(grep {"hello" eq $_} @arr); # return 1 as expected

Solution

  • Perl's syntax for its "functions" that take a block argument is a bit weird, and it's one of my Perl annoyances. There are some things that are just weird because that's how Perl does it:

    grep {...} @array;   # no comma, block argument
    
    grep $_ == 4, @array # comma, expression argument
    

    Adding the sub doesn't look like a block argument to Perl simply because that's not the way that Perl parses things:

    grep sub { $_ == 4} @array # missing comma, expression argument, compilation error
    
    grep sub { $_ == 4} @array # comma, expression argument
    

    But this works when you use this special block form with the sub left off, and Perl knows how to parse these special cases because Perl knows how to parse these special cases:

    $ perl -MO=Deparse -e 'my @f = grep { $_ == 4 } @array'
    my(@f) = grep({$_ == 4;} @array);
    -e syntax OK
    
    $ perl -MO=Deparse -e 'my @f = grep $_ == 4, @array'
    my(@f) = grep(($_ == 4), @array);
    -e syntax OK
    
    $ perl -MO=Deparse -e 'my @f = grep sub{$_ == 4}, @array'
    my(@f) = grep(sub {
        $_ == 4;
    }
    , @array);
    -e syntax OK
    
    $ perl -MO=Deparse -e 'my @f = grep sub{$_ == 4} @array'
    Array found where operator expected at -e line 1, near "} @array"
        (Missing operator before  @array?)
    syntax error at -e line 1, near "} @array
    "
    -e had compilation errors.
    

    That's just how it is. I wish Perl had a more general idea of anonymous functions, and that's one of the things that Raku addressed. I think Ruby did a nice job with optional blocks, too.

    Now, let's make our own function, f with a block argument by using prototypes (which is more often than not a the best idea). The situation is slightly different (maddening, I know) for a user-defined function than Perl's builtin stuff:

    Give f a block, no problem:

    $ perl -MO=Deparse -e 'sub f (&) { $_[0]->() }; print f {137}'
    sub f (&) {
        $_[0]->();
    }
    print f(sub {
        137;
    }
    );
    -e syntax OK
    

    Give f a anonymous sub, no problem:

    $ perl -MO=Deparse -e 'sub f (&) { $_[0]->() }; print f sub {137}'
    sub f (&) {
        $_[0]->();
    }
    print f(sub {
        137;
    }
    );
    -e syntax OK
    

    But, use parens and Perl thinks the block is an anonymous hash, even if you try to trick Perl into seeing it as a code block:

    $ perl -MO=Deparse -e 'sub f (&) { $_[0]->() }; print f({137})'
    Type of arg 1 to main::f must be block or sub {} (not anonymous hash ({})) at -e line 1, near "})
    "
    -e had compilation errors.
    sub f (&) {
        $_[0]->();
    }
    print &f({137});
    
    $ perl -MO=Deparse -e 'sub f (&) { $_[0]->() }; print f({137;})'
    syntax error at -e line 1, near ";}"
    -e had compilation errors.
    sub f (&) {
        $_[0]->();
    }
    
    $ perl -MO=Deparse -e 'sub f (&) { $_[0]->() }; print f({return 137})'
    Type of arg 1 to main::f must be block or sub {} (not anonymous hash ({})) at -e line 1, near "})
    "
    -e had compilation errors.
    sub f (&) {
        $_[0]->();
    }
    print &f({(return 137)});
    

    And sometimes that's just the way it is.