Understand why you probably don’t need prototypes

You should understand how Perl’s prototypes work, not so you’ll use them but so you won’t be tempted to use them. Although prototypes can solve some problems, they don’t solve the problems most people want.

Some languages, such as C, have function prototypes. You tell your function how many arguments it has and what sort they are, as well as what type of thing it returns:

char* some_function( int start, int length );

Java has a method signature:

public class SomeClass {
   public String mySubstr( String aString, int i, int j ) {
     ...
   }
 }

Aside from syntax-warping modules, such as Devel::Declare, or source filters, such as Filter::Simple, Perl doesn’t have that as a fundamental feature. It’s subroutines and methods take lists and return lists (or a single item).

Perl does have prototypes as a compile-time aid, documented in perlsub. It’s not there to ensure you give a subroutines particular sorts of arguments but to help the compiler figure out what you typed and how you want it to interpret it. Perl doesn’t require you to surround your arguments in parentheses, so prototypes gives you a way to tell the compiler where the arguments start and end. Consider these examples, which you’ll understand by the end of this Item:

use utf8;

my $value = ? +1;
my @array = ( sin ?*5/2, cos ?, 1, 2, 3 );

A lesser part of that includes the specification of Perl core types (scalar, array, hash, subroutine, or globs). Method calls, which require the parentheses to surround their arguments, completely ignore prototypes because Perl doesn’t need any help to parse them.

You (optionally) specify the prototype after the subroutine name. The simplest prototype is the empty prototype, meaning the subroutine takes no arguments:

use utf8;

sub TRUE  () { 1 }
sub FALSE () { 0 }
sub ?     () { 3.1415926 }

The simplest prototype

The empty prototype tells perl not to consume any arguments when it sees that function name. How does perl know how to interpret this?

use utf8;

say ? +1;

Since ? is a subroutine, it can take arguments. Since you wrote it without parentheses, perl needs a hint to parse that. It could be two forms, each with possibly different answers:

use utf8;

say ?( +1 );
say ?() + 1;

The empty prototype tells perl to parse it as ?() + 1. This makes the empty prototype a way that you can declare constants.

This means, however, that perl needs to know about the prototype before it parses that bit of code. This works because the prototype shows up first because the subroutine is completely defined before it’s called:

use utf8;

sub ? () { 3.1415926 }
say ? +1;   # 4.1415926

You don’t need to define the subroutine ahead of time, but you have to declare its prototype to get the behavior that you expect:

use utf8;

sub ? ();
say ? +1;   # 4.1415926

BEGIN {
*? = sub { 3.1415926 }
}

This isn’t a recommendation to write code like this, but it illustrates the point. Even with warnings turned off, you’ll still get a warning about the mismatch in prototypes:

use v5.10;
use utf8;

sub ? ();
say ? +1;   # 4.1415926

BEGIN {
*? = sub { 3.1415926 }
}

You can make the prototypes in the forward definition and the full definitions match:

use v5.10;
use utf8;

sub ? ();
say ? +1;   # 4.1415926

BEGIN {
	*? = sub (){ 3.1415926 }
	}

The prototype matters only for the calls to the subroutines after its definition. The prototype doesn’t matter for subroutine calls before its definition. However, to use the subroutine as a bareword, you still have to have a forward declaration:

use v5.10;
use utf8;

sub ?;
say ? +1;   # 3.1415926

sub ? ();
say ? +1;   # 4.1415926

BEGIN {
	*? = sub (){ 3.1415926 }
	}

Not only that, we can change the prototype as perl parses your program.

use v5.10;
use utf8;

sub ?;
say ? +1;

sub ? ();
say ? +1;   # 4.1415926

sub ? ($$);
say ? +1;   

BEGIN {
	*? = sub (){ 3.1415926 }
	}

The sub ? ($$) tells perl to expect two arguments for the subsequent calls. Since you don’t give it enough arguments

Prototype mismatch: sub main::? () vs ($$) at proto.pl line 10.
Not enough arguments for main::? at proto.pl line 11, near "1;"
BEGIN not safe after errors--compilation aborted at proto.pl line 15.

More than zero arguments

To take more or more arguments, you just specify that number of items in the prototype. So far, you’ve only seen scalar arguments, which you specify as a $ in the prototype:

sub twofer    ($$);    # exactly two arguments
sub hat_trick ($$$);   # exactly three arguments

This does not mean that the subroutine gets that number of arguments. It does not mean that it takes two scalar variables as arguments. Perl parses each argument in scalar context:

use v5.10;

sub twofer ($$) { say "@_" };

my @array = qw( Buster Mimi Roscoe );
twofer @array, 2;

my %hash = map { $_ => 1 } 'a' .. 'z';
twofer %hash, 2;

The @array is a single argument, the first one, and is taken in scalar context, giving the number of elements in it. The %hash is treated in the same way, providing the mostly useless “hash statistics” scalar value:

3 2
19/32 2

Putting a \ in front of a prototype character specifies that the argument is a named variable. Instead of the value, you get a reference to the value:

use v5.10;

sub twofer (\$) { say "@_" };

my $scalar = 'Buster';
twofer $scalar;

The output shows the reference:

SCALAR(0x10082e548)

If you try to give it a non-variable, perl complains at compile-time:

Type of arg 1 to main::twofer must be scalar (not constant item) at proto.pl line 6, near "2;"
Execution of proto.pl aborted due to compilation errors.

A non-backslashed @ in a prototype specifies a list and forces list context on the rest of the arguments. It does not require and array argument:

use v5.10;

sub twofer (@) { say "@_" };

my @array = qw( Buster Mimi Roscoe );
twofer @array, 2;

my %hash = map { $_ => 1 } 'a' .. 'z';
twofer %hash, 2;

The output shows the normal list flattening behavior you expect from a Perl subroutine call. Notice that it does not care about the number or type of arguments:

Buster Mimi Roscoe 2
w 1 r 1 a 1 x 1 d 1 j 1 y 1 u 1 k 1 h 1 g 1 f 1 t 1 i 1 e 1 n 1 v 1 m 1 s 1 l 1 c 1 p 1 q 1 b 1 z 1 o 1 2

If you wanted to keep the array together, you would put a backslash in front of the \@. The argument must be a named array, and not an anonymous array or a reference to an array. Even though the argument is an array, the value in @_ will be a reference to that array:

use v5.10;

sub twofer (\@$) { say "@_" };

my @array = qw( Buster Mimi Roscoe );
twofer @array, 2;

The output shows two arguments:

ARRAY(0x100827810) 2

You can’t then sneak in a scalar variable or a hash variable:

use v5.10;

sub twofer (\@$) { say "@_" };

my @array = qw( Buster Mimi Roscoe );
twofer @array, 2;

my %hash = map { $_ => 1 } 'a' .. 'z';
twofer %hash, 2;

perl catches that at compile-time:

Type of arg 1 to main::twofer must be array (not private hash) at proto.pl line 9, near "2;"
Execution of proto.pl aborted due to compilation errors.

If you want to take more than one type of argument at a particular position, you can specify the possible types in brackets. To take either an array or a hash, you use [@%]:

use v5.10;

sub twofer (\[@%]$) { say "@_" };

my @array = qw( Buster Mimi Roscoe );
twofer @array, 2;

my %hash = map { $_ => 1 } 'a' .. 'z';
twofer %hash, 2;

Now the output takes either:

ARRAY(0x100827810) 2
HASH(0x10082e0c8) 2

If you want to take two separate arrays,

use v5.10;

sub twofer (\@\@) { say "@_" };

my @array1 = qw( Buster Mimi Roscoe );
my @array2 = qw( Ginger Ellie );
twofer @array1, @array2;

You get one reference for each array:

ARRAY(0x100827810) ARRAY(0x10082dff0)

Even though you can specify the variable type with the backslashed form, you can’t specify anything about the values that they hold, or limits to the number of elements they contain.

You can also specify prototypes for subroutines and globs, which we’ll cover in a separate Item since you can have a lot more fun with those.

Optional arguments

So far, you’ve used prototypes that specify an exact number of elements. If you want to specify optional arguments, you can divide the mandatory and optional prototype characters with a semicolon. If you wanted to take at least two but possible three arguments, you’d use the prototype ($$;$)

use v5.10;

sub hat_trick ($$;$) { say "@_" };

hat_trick 'Buster', 'Mimi';
hat_trick 'Buster', 'Mimi', 'Roscoe';

Both of those work just fine, but if you try to give it four arguments, you get a compilation error telling you that there are “Too many arguments”.

If you want a minimum number of arguments, but no maximum, you can use the @ as the optional argument. All of these are fine:

use v5.10;

sub hat_trick ($$;@) { say "@_" };

hat_trick 'Buster', 'Mimi';
hat_trick 'Buster', 'Mimi', 'Roscoe';
hat_trick 'Buster', 'Mimi', 'Roscoe', 'Ginger';

Here are some interesting prototypes from List::MoreUtils:

# from List::MoreUtils
sub each_array (\@;\@\@\@\@\@\@\@\@\@\@\@\@\@\@\@\@\@\@\@\@\@\@\@\@)

sub natatime ($@)

sub mesh (\@\@;\@\@\@\@\@\@\@\@\@\@\@\@\@\@\@\@\@\@\@\@\@\@\@\@\@\@\@\@\@\@)

A final warning

Subroutine prototypes exist chiefly so perl can parse calls to your subroutines just like it would its built-insówithout parentheses. They can set the context for the arguments or the variable types, but they can’t specify the sorts of values. Prototypes aren’t the tools that you want if any of those are your goal. It’s also easier to just use parentheses to mark your argument list.

Things to remember

  • Prototypes are not function signatures
  • Non-backslashed prototype characters enforce a context, not a type
  • Backslashed prototype characters enforce a variable type
  • You can specify optional arguments after a semicolon in the prototype.

Post to Twitter Post to Delicious Post to Digg Post to Facebook Post to Reddit

Leave a comment

1 Comments.

  1. Thank you for your easy and kind articles, always.

    I have a question about this article. What does the prototype of “each_array” mean, in List::MoreUtils? (same about “mesh”)

    At first, I though that each_array could receive only up to 25(1+24optional) arguments, that is, array variables. However I tested it a moment ago and it worked well with 30 arguments.

Leave a Reply

You must be logged in to post a comment.