Use v5.20 subroutine signatures

Subroutine signatures, in a rudimentary form, have shown up in Perl v5.20 as an experimental feature. After a few of years of debate and a couple of competing implementations, we have something we can use. And, because it was such a contentious subject, it got the attention a new feature deserves. They don’t have all the features we want (notably type and value constraints), but Perl is in a good position to add those later.

Perl’s roots were in simplicity and getting started as quickly as possible. We wanted to define subroutines without much work. Instead of creating signatures in a C header file and worrying about inputs and outputs, Larry made subroutines take in lists and return lists. Done and done.

This simplicity means you have to do quite a bit of work yourself. You have to process the input list, in @_, assign your own default values, and declare the variables to possibly store them. You ended up not saving much for the typical programmer.

To start, you need to enable the experimental feature (see Item 2. Enable new Perl features when you need them. and turn off the experimental warnings):

use v5.20;
use feature qw(signatures);
no warnings qw(experimental::signatures);

To define a signature, you use the spot after the subroutine name where so far Perl has only allowed a prototype. This is not a prototype though; it’s something different. Notably, the new subroutine signatures work with methods, whereas prototypes are compiler hints that method calls ignore:

use v5.10;

sub some_sub ($$) {
	say "I got $_[0] and $_[1]";
	}

some_sub( 'Buster', 'Mimi' );

main->some_sub();             # "works" just fine

some_sub( qw(Buster) );       # compile time error

But, you have a limited set of characters you can put in a prototype, and those exclude identifier characters (those we use to make names). If you’ve enabled this experimental feature and Perl see un-prototype like characters, it tries signatures instead. Note that Perl has another feature like this: the diamond operator, <>, which might actually be the glob operator if Perl sees glob characters in the argument. You can still use a prototype with signatures, but you probably shouldn’t use prototypes. The perlsub documentation shows you how you can use an attribute to make a prototype.

First, be aware that using a signature does not mess with the normal argument list in @_. You can still play with that yourself.

The simplest prototype

The simplest signature is like the simplest prototype. Like prototypes, the signature enforces the number of arguments. To make a constant in Perl you can use a subroutine that takes no arguments. This is essentially what the constant pragma does:

use v5.20;
use feature qw(signatures);
no warnings qw(experimental::signatures);

sub cat () { 'Buster' }

say "Cat is ", cat;

If you try to pass an argument, you’ll get an error but at runtime:

use v5.20;
use feature qw(signatures);
no warnings qw(experimental::signatures);

sub cat () { 'Buster' }
say "Running...";
say "Cat is ", cat( 'Mini');

The first say works, but the second fails when it calls cat incorrectly:

Running...
Too many arguments for subroutine at cat.pl line 7.

A prototype would have raised a compile-time error because the compiler already knows how many arguments there should be. This doesn’t mean that the experimental signatures might do that later, but the implementation now is more of a code mangler. Deparsing it (Use B::Deparse to see what perl thinks the code is) shows that the cat subroutine has a die triggered by a check of @_:

$ perl5.20.0 -MO=Deparse cat.pl
sub BEGIN {
    require v5.20;
}
sub cat {
    BEGIN {${^WARNING_BITS} = "\020\001\000\000\000P\004\000\000\000\000\000\000U\005"}
    use strict;
    use feature 'current_sub', 'evalbytes', 'fc', 'say', 'signatures', 'state', 'switch', 'unicode_strings', 'unicode_eval';
    no feature 'array_base';
    die 'Too many arguments for subroutine' unless @_ <= 0;
    ();
    'Buster';
}
BEGIN {${^WARNING_BITS} = "\020\001\000\000\000P\004\000\000\000\000\000\000U\005"}
use strict;
use feature 'current_sub', 'evalbytes', 'fc', 'say', 'signatures', 'state', 'switch', 'unicode_strings', 'unicode_eval';
no feature 'array_base';
say 'Cat is ', cat('Mini');
test.pl syntax OK

Don't get too hung up on that because it might be a temporary implementation detail. This does mean, however, that you can catch this error with eval:

use v5.20;
use feature qw(signatures);
no warnings qw(experimental::signatures);

sub cat () { 'Buster' }
say "Running...";
say "Cat is ", eval { cat( 'Mini') };

say "Caught error: $@" if $@;

Now we catch the error, but notice it comes from the line of the subroutine definition, not the point where you called the subroutine like you would with a croak:

Running...
Cat is 
Caught error: Too many arguments for subroutine at cat.pl line 5.

Mandatory, positional parameters

The meat of this feature is your ability to assign to variables in what many perceive as a prettier way. Instead of declaring variables, usually with my, and performing list operations on @_, you list the variables in the signature in the order you would assign from @_:

use v5.20;
use feature qw(signatures);
no warnings qw(experimental::signatures);

cat( 'Buster' );

sub cat ($cat) { 
	say "The cat is $cat";
	}

Again, this checks the number of parameters. With no arguments or more than one argument you get a runtime error.

You separate multiple arguments with commas, just like a list assignment:

use v5.20;
use feature qw(signatures);
no warnings qw(experimental::signatures);

animals( 'Buster', 'Nikki', 'Godzilla' );

sub animals ($cat, $dog, $lizard) { 
	say "The cat is $cat";
	say "The dog is $dog";
	say "The lizard is $lizard";
	}

These variables are lexical variables in the scope of the subroutine (as you'd see if you deparsed this code). That means you can't assign to special variables, which would cause an compile-time error. That is, except for $_, which is experimentally lexical from a v5.10 misadventure with given-when (Perl v5.16 now sets proper magic on lexical $_ and Use for() instead of given()).

Placeholder parameters

You don't have to name all of the parameters. You can use the lone $ to not immediately assign a value, probably because you'll process it yourself through @_. In this example we don't care about the second argument, but the signature still needs the right number of positions and in the right sequence:

use v5.20;
use feature qw(signatures);
no warnings qw(experimental::signatures);

animals( 'Buster', 'Nikki', 'Godzilla' );

sub animals ($cat, $, $lizard) {  # unnamed second parameter
	say "The cat is $cat";
	say "The lizard is $lizard";
	}

This is a bit tricky really. That second argument is mandatory even though you've neglected to give it a name. It will still be in @_ even though you haven't assigned it to a variable.

Slurpy parameters

At the end of the parameter list, you can have a slurpy parameter, which is either a named array or hash. You can do this is a list assignment too, but a list assignment lets you put it in the middle despite the fact that any succeeding elements get nothing:

my( $cat, $dog, @animals, $lizard ) # $lizard will never get anything
	= qw( Buster Nikki Ginger Mimi Godzilla );

In the subroutine signature, that slurpy thing has to be at the end of the list:

sub animals ( $cat, $dog, @animals ) { # @animals must be at the end
	...;
	}

The rest of the arguments past the second show up in @animals. But, here's the interesting thing; the number of things that can show up in the array can be zero.

Sometimes you may not want to completely specify the number of arguments that your subroutine may take, but you also don't want to create a named array, you can use a bare @ as placeholder to mean that the argument list is unlimited:

sub animals ( $cat, $dog, @ ) { # @ slurps the rest
	...;
	}

The hash can also be a slurpy parameter, and just like the slurpy array it must be at the end of the signature:

sub animals ( $cat, $dog, %args ) { # %args demands an even number
	...;
	}

For the hash, if there isn't an even number of elements left in @_, you'll get a runtime exception. You don't have to name the hash, and a bare % still demands an even number of elements:

sub animals ( $cat, $dog, % ) { # % still demands an even number
	...;
	}

Default values (optional parameters)

Perhaps the best feature of signatures are default values. You can use Perl to decide what the default values, even if that is a literal. However, you can only assign default values to optional parameters, which means that they have to appear after the mandatory arguments. In this example, the third argument is optional and gets the default value 'MechaGodzilla' when no argument is present:

use v5.20;
use feature qw(signatures);
no warnings qw(experimental::signatures);

say "First try-----------";
animals( 'Mimi', 'Nikki', 'Godzilla' );

say "Second try-----------";
animals( 'Buster', 'Nikki', );

sub animals ( $cat, $dog, $lizard = 'MechaGodzilla' ) {
	say "The cat is $cat";
	say "The dog is $dog";
	say "The lizard is $lizard";
	}

On the second try, you get the default value:

First try-----------
The cat is Mimi
The dog is Nikki
The lizard is Godzilla
Second try-----------
The cat is Buster
The dog is Nikki
The lizard is MechaGodzilla

This is only checking the number of arguments and assigning a value when the argument list is too short. If you pass undef as an argument, that's the (un)value that parameter will get:

use v5.20;
use feature qw(signatures);
no warnings qw(experimental::signatures);

say "First try-----------";
animals( 'Mimi', 'Nikki', 'Godzilla' );

say "Second try-----------";
animals( 'Buster', 'Nikki', );

say "undef try-----------";
animals( 'Buster', 'Nikki', undef );

sub animals ( $cat, $dog, $lizard = 'MechaGodzilla', $bird = 'Poppy' ) {
	say "The cat is $cat";
	say "The dog is $dog";
	say "The lizard is $lizard";
	say "The bird is $bird";
	}

The undef does not trigger a default value, which may surprise many of you. Notice the third, "undef try" where $lizard gets no value:

First try-----------
The cat is Mimi
The dog is Nikki
The lizard is Godzilla
The bird is Poppy
Second try-----------
The cat is Buster
The dog is Nikki
The lizard is MechaGodzilla
The bird is Poppy
undef try-----------
The cat is Buster
The dog is Nikki
The lizard is 
The bird is Poppy

You can also have a null default value. You might want to have one optional parameter but not assign a value if the argument list isn't long enough. This signature allows one or two arguments, with no defaults:

use v5.20;
use feature qw(signatures);
no warnings qw(experimental::signatures);

say "First try-----------";
animals( 'Mimi', 'Nikki' );

say "Second try-----------";
animals( 'Buster' );

sub animals ( $cat, $= ) {  # second argument is optional
	say "The cat is $cat";
	say "Second argument is $_[1]" if $#_ == 1;
	}

You see that the second argument only exists if you specify it yourself:

First try-----------
The cat is Mimi
Second argument is Nikki
Second try-----------
The cat is Buster

These default values don't work with slurpy types, though. Perl will complain at compile-time. If you want that sort of thing, though, you can make the argument a scalar and assign an array or hash reference to it:

use v5.20;
use feature qw(signatures);
no warnings qw(experimental::signatures);

animals( 'Buster' );

sub animals ( $cat, $hash={} ) {  # hashref argument is optional
	say "The cat is $cat";
	...;
	}

Fancy optional values

So far your defaults have been simple values, but you can use Perl. That means you can do almost anything. This one uses the value in another variable and increments it as it assigns defaults:

use v5.20;
use feature qw(signatures);
no warnings qw(experimental::signatures);

animals( 'Mimi' );
animals( 'Buster' );

{
my $auto_id = 0;
sub animals ( $cat, $id = ++$auto_id ) {
	say "$id: The cat is $cat";
	}
}

Each cat automatically gets its own sequence value since the animals subroutine closed over $auto_id:

1: The cat is Mimi
2: The cat is Buster

However, you can't do something tricky to bring $auto_id into the subroutine since the parser doesn't know about the variable soon enough. Neither of these work:

sub animals ( $cat, $id = do { state $auto_id = 0; $auto_id++ } ) {
	say "$auto_id: The cat is $cat";
	}

sub animals ( $cat, $id = $auto_id++ ) {
	state $auto_id++;
	say "$auto_id: The cat is $cat";
	}

You can make the default value a call to a subroutine:

use v5.20;
use feature qw(signatures);
no warnings qw(experimental::signatures);

animals( 'Mimi' );
animals( 'Buster' );

sub animals ( $cat, $auto_id = get_id() ) {
	say "$auto_id: The cat is $cat";
	}

sub get_id () {
	state $id = 0;
	++$id;
	}

Method signatures

My favorite part of signatures is that they work with methods. This doesn't mean that we have multi-dispatch in Perl (yet) (well, Perl 6 does but that's a different language). The signatures aren't any different; they follow all the same rules:

use v5.20;

package Local::Animals {
	use feature qw(signatures);
	no warnings qw(experimental::signatures);

	sub new ( $class, $args = {'name' => 'Buster'} ) {
		bless $args, $class
		}

	sub name ( $self ) {
		$self->{name};
		}
	}

my $cat = Local::Animals->new;
say "Cat is ", $cat->name;

A quick guide

Exactly zero arguments - empty signature ()
Exactly one named argument ($cat)
Zero or one arguments, with no default, unnamed ($=)
Zero or one arguments, with no default, named ($cat=)
Zero or one arguments, with a default ($cat='Buster')
Exactly two arguments, ($cat, $dog)
Two or more arguments, any number ($cat, $dog, @)
Two or more arguments, but an even number ($cat, $dog, %)
Two or more arguments, slurping rest into an array ($cat, $dog, @animals)
Two or more arguments, slurping rest into an hash ($cat, $dog, %animals)
Two, three, or four arguments, no defaults ($cat, $dog, $=, $=)
Two, three, or four arguments, one default ($cat, $dog, $lizard='Godzilla', $=)
Class method ( $class, ... )
Object method ( $self, ... )

Things to Remember

  • Subroutine signatures are experimental
  • Signatures are not prototypes
  • Signatures enforce the number of arguments, but at runtime
  • Default values only apply to optional parameters, and only apply to parameters past the number of actual passed arguments

14 thoughts on “Use v5.20 subroutine signatures”

  1. Is there any way to introspect the subroutine reference to see the names and types of its arguments?

  2. After playing a bit with this feature, it seems that signatures take a copy of @_ rather than aliasing it.

    So they don’t provide an elegant replacement to:

    sub swap($$) { 
    for my $x ( $_[0] ) { for my $y ( $_[1] ) {
        ($x,$y) = ($y,$x);
    }}
    }
    

    I was expecting signatures to do aliasing of @_, not copying.

    1. Although there is a new Perl feature to alias different names to the same data, I did not expect signatures to alias data. I don’t see how that would work, either. I think there would be more confusion that way.

  3. Also the single-line constraint can become a problem with code readability (if one wants to comment the parameters) and can be easily broken by perltidy unfortunately.

  4. Subroutine signatures is an excellent feaature, but possibility to make aliases is highly required.

    For example:

    sub ( $a, \$b, \@c ), where

    $a – is a copy of $_[0];
    $b – alias for $_[1];
    @c – ($_[2] .. $#_ );

    For \%hash I don’t know what to do, maybe this can be left unimplemented.

    Maybe you can suggest this for perl core devs?

    1. The subroutine signature feature has several more things waiting for implementation and I think they want to figure that stuff out before they make it stable.

  5. Hello,

    What is the future or roadmap of signatures? As of now 5.26 is out and the perldeltas mention speed improvements. It sounds like signatures are here to stay, but is it still experimental? How heavily can we rely on it when using perl 5.22 for example.

    1. I haven’t kept up with the state of signatures. They are still experimental because there are a few more features they’d like to squeeze into them. That might change the current syntax and they don’t want to handcuff themselves to that. I think they’re pretty close to how they’ll end up though. At least I hope so because I use them almost everywhere!

      1. Thank you for the response. I’m hoping the simple use cases stay as they appear to be very useful. Specially for methods.

Comments are closed.