fuzzix dot org

Perl signatures, block parameters, and prototypes

4 Jan 2023

With Perl version 5.36, released in May 2022, the signatures feature is no longer considered experimental and is enabled in that version bundle (i.e. if you use v5.36, the signatures feature will be enabled).

use v5.36;

sub signatures( $hooray = undef ) {
    say $hooray // 'Hooray!';
}

There is a simpler but related feature in Perl called prototypes, which allows you to declare the types of parameters though does not allocate names for these parameters. The default list @_ must still be unrolled. This feature is often used for compile-time parameter type checking, though it is more appropriately used for creating functions with builtin-style calling conventions. A common example is accepting a code block (rather than an anonymous sub) as a parameter, which allows calling without parentheses in the style of grep or map. This example will, perhaps unwisely, apply the block passed to transform to the supplied list:

sub transform (&@) {
    my $code = shift;
    $code->( $_ ) for @_;
}

my @list = 1, 2, 3;
transform { ++$_ } @list; # @list now contains 2, 3, 4

If we naively upgrade this script by adding use v5.36, this form of the prototypes feature will now throw a syntax error: "A signature parameter must start with '$', '@' or '%'". So, how might one define a subroutine with a signature, which acts equivalently to a prototype? My very first thought was something like:

sub transform( &code, @list ) {
    &code( $_ ) for @list;
}

... but as we see from the syntax error above & is not one of the allowed sigils! This invocation also would not allow for the builtin-style calling convention without parentheses. You would need to call transform( sub { ... }, @list ).

Thankfully an alternative mechanism exists for declaring prototypes, the prototype attribute. A first pass at applying a prototype alongside a signature might look like this:

use v5.36;

sub transform :prototype(&@) ( $code, @list ) {
    $code->( $_ ) for @list;
}

One problem remains. The @list handled inside transform is now a copy of the passed values, so operations in the block are not propagated to the contents of the original array. That is:

my @list = 1, 2, 3;
transform { ++$_ } @list; # @list still contains 1, 2, 3

We can use the prototype to coerce @list to be an array ref - effectively a pointer to the passed array - and dereference this array ref within transform. The complete code now looks like this:

use v5.36;

sub transform :prototype(&\@) ( $code, $list ) {
    $code->( $_ ) for $list->@*;
}

my @list = 1, 2, 3;
transform { ++$_ } @list; # @list now contains 2, 3, 4

The old and the new are joined in harmony (Currently playing: Boards of Canada). I am certain this is all explained in documentation somewhere, but I need some excuse to update the site.

Update 5 Jan 2023 An update from Paul Evans on the possibility of parameter attributes in core states:

@fuzzix By default signature args are copies but there are vague thoughts ahead of one day doing an attribute for other behaviours. E.g.

sub transform :prototype(&@) ( $code, @values :alias ) {
       ...
}

This is certainly interesting. It looks like attributes, rarely used outside frameworks like Catalyst, may be about to become a lot more powerful and useful.

Attributes are also being applied to fields in Corinna (Corinna is the ongoing effort to specify the upcoming feature 'class' in core Perl). Paul is also working on the implementation for this and I am excited to see how it works out. If you want to play with these ideas now, check out Object::Pad.