Prototypes Are NOT Type-Checking
Pop quiz. What does this prototype do?This page explains what Perl prototypes actually do, and more importantly, what they do not do. A common guess is thatsub add ($$) { return $_[0] + $_[1]; }
($$) validates that two scalars arrive at your function. That assumption is wrong, and it trips up almost every Perl programmer at some point. Prototypes never check types or validate arguments at runtime. They are compile-time parsing hints that change how Perl reads the call site. They also disappear entirely when you call them as methods.
Part 1: WHAT PEOPLE THINK PROTOTYPES DO
The word prototype borrows baggage from C, C++, and Java. Those languages use a function prototype to declare parameter types so the compiler can catch mistakes. Perl programmers see($$) and assume it enforces exactly two scalar arguments. The reality is much simpler. The
specifier tells Perl to treat the first two arguments as scalar expressions during parsing. It changes how Perl reads your code. It does not change how Perl runs your code.sub multiply ($$) { return $_[0] * $_[1]; }
Part 2: WHAT PROTOTYPES ACTUALLY DO
Prototypes are compile-time parsing hints that tell the Perl parser how to interpret the argument list at the call site.The ($) prototype forces scalar context on the argument:
Without the prototype:sub count_items ($) { say "Got: $_[0]"; } my @arr = (1, 2, 3, 4, 5); count_items(@arr); # prints "Got: 5" (array in scalar context = count)
The difference shows up at the call site. Withsub count_items { say "Got: $_[0]"; } my @arr = (1, 2, 3, 4, 5); count_items(@arr); # prints "Got: 1" (array flattened, first element)
($), Perl evaluates @arr in scalar context before the function even sees it. The function receives the number 5, not the list (1, 2, 3, 4, 5). No type checking happened. Perl just parsed the call differently.
Part 3: THE (@) AND (\@) DISTINCTION
The(@) prototype is a no-op. It means "slurp everything as a list." That is what Perl does by default anyway.
Thesub show_all (@) { say for @_; }
(\@) prototype, with the backslash, is where things get interesting. It means "take a reference to the array, do not flatten it":
Without thesub show_ref (\@) { my ($aref) = @_; say "Array has " . scalar @$aref . " elements"; } my @nums = (10, 20, 30); show_ref(@nums); # prints "Array has 3 elements"
(\@) prototype, you would have to pass the reference explicitly:
The prototype lets you callshow_ref(\@nums); # without prototype, you need the backslash
show_ref(@nums) and Perl automatically takes the reference for you at compile time. The function receives an array reference, not a flattened list.
This is how push works. It is a builtin with an implicit (\@) prototype. That is why push @array, $value works without you writing push \@array, $value.
Part 4: WHY PROTOTYPES ARE IGNORED ON METHOD CALLS
Here is the killer. Method calls completely ignore prototypes:Five arguments. The prototype says two. Perl does not care.package Calculator; sub new { bless {}, shift } sub add ($$) { my ($self, $a, $b) = @_; return $a + $b; } package main; my $calc = Calculator->new; $calc->add(1, 2, 3, 4, 5); # no error, no warning
The reason is that method calls resolve at runtime through method dispatch. The prototype is a compile-time hint. At compile time, Perl does not know which add method will be called. It could live in the current class, a parent class, or load dynamically. Since Perl cannot know which function the method resolves to, it cannot apply its prototype.
So it does not try.
Part 5: WHY OO CODE NEVER USES THEM
This is why you almost never see prototypes in object-oriented Perl. They literally do nothing on method calls. Every method call takes the form$obj->method(args) or Class->method(args), and prototypes are ignored for both.
Moose, Moo, and every other OO framework ignore prototypes. The entire modern Perl OO ecosystem pretends they do not exist. They are right to do so.
Do not put prototypes on methods. They give readers false confidence that argument checking is happening. It is not.# These prototypes accomplish nothing: package MyClass; sub new ($) { ... } # ignored on MyClass->new sub process ($$) { ... } # ignored on $obj->process(...) sub config (\%) { ... } # ignored on $obj->config(...)
Part 6: WHEN PROTOTYPES ARE USEFUL
Prototypes have one legitimate purpose: making your subroutines behave like builtins.Want a function that takes a block like map or grep?
Thesub apply_to_each (&@) { my $code = shift; my @results; for (@_) { push @results, $code->($_); } return @results; } my @doubled = apply_to_each { $_ * 2 } 1, 2, 3, 4; say join ", ", @doubled; # 2, 4, 6, 8
(&@) prototype says "first argument is a code block, rest is a list." This lets you write apply_to_each { ... } @list without the sub keyword on the block. Just like map { ... } @list.
Without that prototype, you would need:
The prototype makes the syntax cleaner. That is it. That is the whole benefit. No type checking. No argument validation. Just nicer syntax.my @doubled = apply_to_each(sub { $_ * 2 }, 1, 2, 3, 4);
Part 7: THE (;) OPTIONAL MARKER
A semicolon in a prototype marks the boundary between required and optional arguments. Well, "required" is a strong word. It is more like "the parser expects these, and the rest are optional."Thesub greet ($;$) { my ($name, $greeting) = @_; $greeting //= "Hello"; say "$greeting, $name!"; } greet("World"); # Hello, World! greet("World", "Howdy"); # Howdy, World!
($;$) says "one mandatory scalar, one optional scalar." But again, this is a parser hint. Perl will not die if you pass zero arguments or ten. You will just get undef values and possibly a "Use of uninitialized value" warning.
Part 8: COMMON MISTAKES
Mistake 1: Using prototypes for argument validation.The# WRONG: this does not validate anything sub safe_divide ($$) { my ($a, $b) = @_; die "division by zero" if $b == 0; return $a / $b; }
($$) does not ensure two arguments are passed. If you call safe_divide(42), Perl happily passes 42 and undef.
For real argument validation, check manually:
Mistake 2: Prototype on a method.sub safe_divide { die "Need exactly 2 arguments" unless @_ == 2; my ($a, $b) = @_; die "division by zero" if $b == 0; return $a / $b; }
Mistake 3: Assuming (\@) validates the type.# WRONG: prototype is ignored on method calls sub process ($$) { my ($self, $data) = @_; ... }
Thesub sum_array (\@) { my ($aref) = @_; my $total = 0; $total += $_ for @$aref; return $total; } sum_array(@numbers); # works sum_array("hello"); # compile-time error, but not a "type check"
(\@) forces a reference to be taken. It is a parsing transformation, not a type assertion.
Part 9: THE MODERN ALTERNATIVES
If you want actual argument validation, use signatures (Perl 5.20+):Signatures do real argument count checking at runtime. They die with a clear error message if the count is wrong. They are not prototypes. They are a different feature entirely.use feature 'signatures'; sub add ($a, $b) { return $a + $b; } add(1, 2); # fine add(1); # dies: Too few arguments add(1, 2, 3); # dies: Too many arguments
Or use a module like Params::Validate:
Real validation. Real error messages. Actually checks types.use Params::Validate qw(validate SCALAR); sub add { my %p = validate(@_, { a => { type => SCALAR }, b => { type => SCALAR }, }); return $p{a} + $p{b}; }
Part 10: THE COMPLETE PICTURE
Here is a reference card. What each prototype character actually does at the call site:CHARACTER PARSING EFFECT AT CALL SITE --------- ------------------------------------------ $ forces scalar context on the argument @ slurps remaining args as list (default) % slurps remaining args as key/value pairs & first arg is a code block (no sub keyword) * accepts a bareword, constant, scalar, or typeglob \$ takes a reference to a scalar \@ takes a reference to an array \% takes a reference to a hash \& takes a reference to a named subroutine ; separates mandatory from optional args _ like $ but defaults to $_ if omitted NONE of these check types at runtime. NONE of these are enforced on method calls. ALL of them are compile-time parsing hints.
The takeaway is straightforward. Do not use prototypes for argument validation. They do not do that. Use them when you want your function to parse like a builtin. Use signatures or validation modules when you want actual checking. And never put them on methods.+------------------+ | PROTOTYPES | | are NOT about | | what goes IN | | | | they're about | | how Perl READS | | the call site | +------------------+ | v .--. |o_o | "Parse hints, not guardrails." |:_/ | // \ \ (| | ) /'\_ _/`\ \___)=(___/
perl.gg