<!-- category: hidden-gems -->
Prototypes Are NOT Type-Checking
Pop quiz. What does this prototype do?If you said "it checks that two scalars are passed," you're wrong. So is almost everyone else. Don't feel bad. This is one of the most widely misunderstood features in all of Perl.sub add ($$) { return $_[0] + $_[1]; }
Prototypes do not check types. They do not validate arguments at runtime. They change how Perl parses the call site at compile time. They are hints to the parser, not guards on your function.
And they are silently ignored on method calls.
Part 1: WHAT PEOPLE THINK PROTOTYPES DO
The name "prototype" is the problem. In C, C++, Java, and every statically typed language, a function prototype declares parameter types. The compiler uses it to catch type errors.So when Perl programmers see this:
They assumesub multiply ($$) { return $_[0] * $_[1]; }
($$) means "this function takes exactly two scalars,
and Perl will complain if you pass anything else." Reasonable guess.
Completely wrong.
What ($$) actually means: "when parsing a call to this function,
treat the first two arguments as scalar expressions." It changes how
Perl reads your code, not how it runs your code.
Part 2: WHAT PROTOTYPES ACTUALLY DO
Prototypes are compile-time parsing hints. They 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)
See the difference? The prototype changed whatsub count_items { say "Got: $_[0]"; } my @arr = (1, 2, 3, 4, 5); count_items(@arr); # prints "Got: 1" (array flattened, first element)
@arr means at the
call site. With ($), 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's 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, don't 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'd 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's a builtin with an implicit (\@)
prototype. That's why push @array, $value works without you
writing push \@array, $value.
Part 4: WHY PROTOTYPES ARE IGNORED ON METHOD CALLS
Here's 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
Why? Because method calls are resolved at runtime through method
dispatch. The prototype is a compile-time hint. At compile time,
Perl doesn't know which add method will be called. It could be in
the current class, a parent class, or loaded dynamically. Since Perl
can't know which function the method resolves to, it can't apply
its prototype.
So it doesn't 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 is$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 don't exist. And they're right to do so.
Don't put prototypes on methods. They give readers false confidence that argument checking is happening. It isn't.# 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'd need:
The prototype makes the syntax cleaner. That's it. That's 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's 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 won't die if you pass zero
arguments or ten. You'll 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's 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's 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: don't use prototypes for argument validation. They don't 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, ever 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