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

3

6

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.

Protect answered 8/11, 2023 at 13:22 Comment(0)
T
2

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 };
Theodolite answered 8/11, 2023 at 14:31 Comment(0)
C
4

& is working fine.

The problems:

  • You have a comma (=>) after the block.
  • + is short for \[@%], and thus wants an array or a hash.
with_some_context { ... } %{ { shift(@ARGV) => 10 } };

No reason to accept an array. And why is the hash optional?

sub with_some_context(&\%) {
   my ( $coderef, $context ) = @_;
   local %ENV = ( %ENV, %$context );
   return $coderef->();
}

with_some_context { ... } %{ { shift(@ARGV) => 10 } };

Do you really want to require a hash?

sub with_some_context(&@) {
   my $coderef = shift;
   local %ENV = ( %ENV, @_ );
   return $coderef->();
}

with_some_context { ... } shift( @ARGV ) => 10;
Como answered 8/11, 2023 at 14:31 Comment(0)
T
2

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 };
Theodolite answered 8/11, 2023 at 14:31 Comment(0)
U
0

There are some Perl builtins can take a block as their first argument. In those cases, there is no separating character (comma) between the block and the next argument:

my @transformed = map { ... } @input;
my @found = grep { ... } @input;

You code does has the comma, so things are different:

with_some_context {...}, ...

Since Perl uses the braces for an anonymous hash as well, Perl sees that you have this first thing in braces followed by this second thing in braces and has to decide what they are. Since the block shouldn't have a comma after it, it must not be a sub block. Perl chooses the other option, and you get all the syntax errors.

Unfrequented answered 8/11, 2023 at 22:1 Comment(3)
Ah, I had assumed that the hashref in the error message referred to the second thing, but maybe it referred to the first thing? And, obviously, I didn't think to put nothing in between the two things.Protect
Re "There are some Perl builtins can take a subroutine as their first argument.", Yes, goto &sub, exists &sub and defined &sub. But not map and grep.Como
Ah, yes, block.Unfrequented

© 2022 - 2024 — McMap. All rights reserved.