Search code examples
perlfunction-prototypes

Why does my Perl function prototype of "(&;+)" still require 'sub'?


I'm playing around with Perl prototypes to learn more about them; I understand that they don't work like most other languages. I don't want them to. I'm specifically looking to get a wrapper function to use a bare block as a code reference, which I think is implied with these two pieces from perlsub:

Because the intent of this feature is primarily to let you define subroutines that work like built-in functions

which I understand to mean that you can drop the parens, and

An & requires an anonymous subroutine, which, if passed as the first argument, does not require the sub keyword or a subsequent comma.

I also want to pass additional arguments to the wrapper function (not to the coderef). It's all syntactic sugar, but that's what I'm after.

I have the following code that I thought would work:

#!/usr/bin/env perl

use strict;
use warnings;

use 5.014;

sub with_some_context (&;+)
{
    my($coderef, $context) = @_;

    {
        local %ENV = %ENV;
        foreach my $key (keys %$context) {
            $ENV{$key} = $context->{$key};
        }

        $coderef->();
    }
}

with_some_context {
    foreach my $key (@ARGV) {
        say("$key: ", (defined $ENV{$key} ? $ENV{$key} : "<undef>"));
    }
} => { shift(@ARGV) => 10 };

with the intent that the { shift(@ARGV) => 10 } hashref is the second argument to the with_some_context function, i.e. $context. The compiler complains and does not use the hashref as the second argument:

$ /tmp/foo SHLVL TERM SHLVL LANG
Useless use of anonymous hash ({}) in void context at /tmp/foo line 26.
SHLVL: 1
TERM: xterm-256color
SHLVL: 1
LANG: en_US.UTF-8

If I put in the word sub, though, the compiler suddenly understands my intent:

with_some_context sub {
    foreach my $key (@ARGV) {
        say("$key: ", (defined $ENV{$key} ? $ENV{$key} : "<undef>"));
    }
} => { shift(@ARGV) => 10 };

as is evidenced when I run it:

$ /tmp/foo SHLVL TERM SHLVL LANG
TERM: xterm-256color
SHLVL: 10
LANG: en_US.UTF-8

If I drop the sub and put in explicit parens around both arguments:

with_some_context({
    foreach my $key (@ARGV) {
        say("$key: ", (defined $ENV{$key} ? $ENV{$key} : "<undef>"));
    }
}, { shift(@ARGV) => 10 });

the compiler completely loses its shit:

$ /tmp/foo SHLVL TERM SHLVL LANG
"my" variable $key masks earlier declaration in same statement at /tmp/foo line 24.
"my" variable %ENV masks earlier declaration in same statement at /tmp/foo line 24.
"my" variable $key masks earlier declaration in same statement at /tmp/foo line 24.
"my" variable @ARGV masks earlier declaration in same statement at /tmp/foo line 26.
syntax error at /tmp/foo line 23, near "foreach "
Execution of /tmp/foo aborted due to compilation errors.

But if I put the sub back:

with_some_context(sub {
    foreach my $key (@ARGV) {
        say("$key: ", (defined $ENV{$key} ? $ENV{$key} : "<undef>"));
    }
}, { shift(@ARGV) => 10 });

or drop the comma and put the parens just around the hashref:

with_some_context {
    foreach my $key (@ARGV) {
        say("$key: ", (defined $ENV{$key} ? $ENV{$key} : "<undef>"));
    }
} ({ shift(@ARGV) => 10 });

it all starts working again:

$ /tmp/foo SHLVL TERM SHLVL LANG
TERM: xterm-256color
SHLVL: 10
LANG: en_US.UTF-8

Running with the sub, without the parens, and without the hashref:

with_some_context {
    foreach my $key (@ARGV) {
        say("$key: ", (defined $ENV{$key} ? $ENV{$key} : "<undef>"));
    }
};

produces no errors, but also doesn't do anything terribly useful:

$ /tmp/foo SHLVL TERM SHLVL LANG
SHLVL: 1
TERM: xterm-256color
SHLVL: 1
LANG: en_US.UTF-8

Clearly I have no understanding of what's going on, as none of this seems consistent to me. I'm very clearly missing something, but I've no clue what.


Solution

  • From perlref:

    Because curly brackets (braces) are used for several other things including BLOCKs, you may occasionally have to disambiguate braces at the beginning of a statement by putting a "+" or a "return" in front so that Perl realizes the opening brace isn't starting a BLOCK. The economy and mnemonic value of using curlies is deemed worth this occasional extra hassle.

    The leading + has the same effect as the parenthesis around the hashref that you tried.

    with_some_context {
      foreach my $key (@ARGV) {
        say("$key: ", (defined $ENV{$key} ? $ENV{$key} : "<undef>"));
      }
    } +{ shift(@ARGV) => 10 };
    

    If you make the key of the hash ref a string, it also works; something about the function call (Or even a bare scalar) makes it parse as something else, causing the error.

    This fails:

    my $first_arg = shift @ARGV;
    with_some_context {
      foreach my $key (@ARGV) {
        say("$key: ", (defined $ENV{$key} ? $ENV{$key} : "<undef>"));
      }
    } { $first_arg => 10 };
    

    but this works

    my $first_arg = shift @ARGV;
    with_some_context {
      foreach my $key (@ARGV) {
        say("$key: ", (defined $ENV{$key} ? $ENV{$key} : "<undef>"));
      }
    } { "$first_arg" => 10 };