Can a Perl subroutine have two different prototypes to allow for an optional block argument?
Asked Answered
F

2

11

Caveats associated with prototypes accepted and notwithstanding, can the two below contrived subs exist within the same package, i.e. to provide an optional block parameter like sort does?

sub myprint {
   for (@_) {
       print "$_\n";
   }
}
sub myprint (&@) {
   my $block = shift;
   for (@_) {
       print $block->() . "\n";
   }
}

The intent is provide a similar calling convention as sort, e.g. to allow execution of:

my @x = qw(foo bar baz);
print_list @x;

# foo
# bar
# baz
 

...and:

my @y = ( {a=>'foo'}, {a=>'bar'}, {a=>'baz'} );
print_list { $_->{a} } @y;

# foo
# bar
# baz

I get redefine and/or prototype mismatch warnings if I try (which is reasonable).

I suppose I can do:

sub myprint {
   my $block = undef;
   $block = shift if @_ && ref($_[0]) eq 'CODE';
   for (@_) {
       print (defined($block) ? $block->() : $_) . "\n";
   }
}

...but the &@ prototype provides the syntactic sugar; removing requires:

my @y = ( {a=>'foo'}, {a=>'bar'}, {a=>'baz'} );
print_list sub { $_->{a} }, @y;                  # note the extra sub and comma

(I've tried ;&@, to no avail -- it still yields Type of arg 1 to main::myprint must be block or sub {} (not private array).)

Furfuraceous answered 14/10, 2014 at 19:13 Comment(2)
What would print_list sub { $_->{a} }, @y do that print_list map { $_->{a} } @y couldn't do?Fairish
@briandfoy - in this contrived, arbitrary example, nothing I can think of, presuming map doesn't introduce additional allocations/memory pressure...Furfuraceous
P
13

Yes.

Unfortunately it's a bit of a pain. You need to use the keyword API introduced in Perl 5.14. This means you need to implement it (and the custom parsing for it) in C and link it to Perl with XS.

Fortunately DOY wrote a great wrapper for the Perl keyword API, allowing you to implement keywords in pure Perl. No C, no XS! It's called Parse::Keyword.

Unfortunately this has major bugs dealing with closed over variables.

Fortunately they can be worked around using PadWalker.

Anyway, here's an example:

use v5.14;

BEGIN {
  package My::Print;
  use Exporter::Shiny qw( myprint );
  use Parse::Keyword { myprint => \&_parse_myprint };
  use PadWalker;
  
  # Here's the actual implementation of the myprint function.
  # When the caller includes a block, this will be the first
  # parameter. When they don't, we'll pass an explicit undef
  # in as the first parameter, to make sure it's nice and
  # unambiguous. This helps us distinguish between these two
  # cases:
  #
  #    myprint { BLOCK } @list_of_coderefs;
  #    myprint @list_of_coderefs;
  #
  sub myprint {
    my $block = shift;
    say for defined($block) ? map($block->($_), @_) : @_;
  }
  
  # This is a function to handle custom parsing for
  # myprint.
  #
  sub _parse_myprint {

    # There might be whitespace after the myprint
    # keyword, so read and discard that.
    #
    lex_read_space;
    
    # This variable will be undef if there is no
    # block, but we'll put a coderef in it if there
    # is a block.
    #
    my $block = undef;
    
    # If the next character is an opening brace...
    #
    if (lex_peek eq '{') {
      
      # ... then ask Parse::Keyword to parse a block.
      # (This includes parsing the opening and closing
      # braces.) parse_block will return a coderef,
      # which we will need to fix up (see later).
      #
      $block = _fixup(parse_block);
      
      # The closing brace may be followed by whitespace.
      #
      lex_read_space;
    }
    
    # After the optional block, there will be a list
    # of things. Parse that. parse_listexpr returns
    # a coderef, which when called will return the
    # actual list. Again, this needs a fix up.
    #
    my $listexpr = _fixup(parse_listexpr);
    
    # This is the stuff that we need to return for
    # Parse::Keyword.
    #
    return (
      
      # All of the above stuff happens at compile-time!
      # The following coderef gets called at run-time,
      # and gets called in list context. Whatever stuff
      # it returns will then get passed to the real
      # `myprint` function as @_.
      #
      sub { $block, $listexpr->() },
      
      # This false value is a signal to Parse::Keyword
      # to say that myprint is an expression, not a
      # full statement. If it was a full statement, then
      # it wouldn't need a semicolon at the end. (Just
      # like you don't need a semicolon after a `foreach`
      # block.)
      #
      !!0,
    );
  }
  
  # This is a workaround for a big bug in Parse::Keyword!
  # The coderefs it returns get bound to lexical
  # variables at compile-time. However, we need access
  # to the variables at run-time.
  #
  sub _fixup {
    
    # This is the coderef generated by Parse::Keyword.
    #
    my $coderef = shift;
    
    # Find out what variables it closed over. If it didn't
    # close over any variables, then it's fine as it is,
    # and we don't need to fix it.
    #
    my $closed_over = PadWalker::closed_over($coderef);
    return $coderef unless keys %$closed_over;
    
    # Otherwise we need to return a new coderef that
    # grabs its caller's lexical variables at run-time,
    # pumps them into the original coderef, and then
    # calls the original coderef.
    #
    return sub {
      my $caller_pad = PadWalker::peek_my(2);
      my %vars = map +($_ => $caller_pad->{$_}), keys %$closed_over;
      PadWalker::set_closed_over($coderef, \%vars);
      goto $coderef;
    };
  }
};

use My::Print qw( myprint );

my $start = "[";
my $end   = "]";

myprint "a", "b", "c";

myprint { $start . $_ . $end } "a", "b", "c";

This generates the following output:

a
b
c
[a]
[b]
[c]
Pushcart answered 14/10, 2014 at 19:41 Comment(2)
Nice Post. I'm debating if I should even try to understand your code. Perhaps save it for a weekend project.Severable
Interesting stuff, if only to remind/convince me of two things: (1) there is more to Perl than I will ever remember; and (2) two distinct methods isn't that big of a deal!! I'll introduce the prototyped-with-block version as myprint_over and have sub myprint { return myprint_over { $_ } @_; }. Thanks.Furfuraceous
A
0

You cannot declare a subroutine with the same syntactic behaviour as sort. To check, try

prototype('CORE::sort')

which returns undef.

Arsenate answered 14/10, 2014 at 19:28 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.