Prototypes#
A prototype is a piece of metadata attached to a sub that controls how the parser parses calls to that sub. It does not control the runtime — it does not check argument counts at runtime, it does not enforce argument types, it does not affect method calls. Its job is to make a user-defined sub parse like a built-in.
That narrow purpose is the one thing to remember about prototypes. Everything else follows.
Declaration#
Two equivalent forms:
sub mypush (\@@) { # short form
my $aref = shift;
push @$aref, @_;
}
sub mypush :prototype(\@@) { # attribute form
...
}
The attribute form is mandatory when signatures are enabled in the same scope, because the parenthesised list after the sub name is then claimed by the signature.
What prototypes actually do#
A prototype rewrites the parser’s expectations at the call site:
sub mygrep (&@) {
my $code = shift;
my @result;
for (@_) { push @result, $_ if &$code }
return @result;
}
mygrep { /foo/ } @items; # parses correctly thanks to (&@)
# without the prototype, this is
# a syntax error
The (&@) prototype tells the parser: “the first argument is a block (no sub keyword needed, no comma after it), the rest is a list.” That’s exactly how built-in grep, map, sort parse.
This is the only place prototypes are useful: building DSL-shaped subs that look like built-ins.
Prototype characters#
Char | Meaning |
|---|---|
| scalar — forces scalar context on the argument |
| list — eats all remaining arguments, list context |
| hash — like |
| block / code ref — first position lets you omit |
| bareword, scalar, typeglob, or globref |
| scalar reference — caller must pass |
| array reference — caller must pass |
| hash reference — caller must pass |
| code reference — caller must pass |
| reference to any of the bracketed kinds |
| scalar OR |
| separates mandatory from optional |
| scalar; if omitted, defaults to |
| no arguments — and triggers the constant-fold optimisation |
The (\@) vs (@) difference#
This is the whole reason backslash prototypes exist.
sub takes_array (\@) { # backslash: pass-by-reference
my ($aref) = @_; # @_ holds [array_ref]
push @$aref, 'extra';
}
sub takes_list (@) { # no backslash: pass-by-value (flattened)
my @copy = @_; # @_ holds the flat list
push @copy, 'extra'; # nothing in caller is affected
}
my @arr = (1, 2, 3);
takes_array(@arr); # caller writes @arr — parser inserts \@arr
# @arr is now (1, 2, 3, 'extra')
takes_list(@arr); # caller writes @arr — flattens to a list
# @arr is unchanged
The (\@) prototype lets you write the call as takes_array(@arr) — looking just like push @arr, ... — while the runtime actually receives an array reference. That’s exactly what built-in push, unshift, splice, and pop look like to the user.
The () constant-fold prototype#
A sub with the empty prototype () and a single-expression body that the optimiser can fold to a constant becomes inline-able:
sub PI () { 3.14159265358979 }
sub MAX () { 100 }
my $area = PI * $r ** 2; # PI is inlined as 3.14159...
This is what use constant produces:
use constant {
PI => 3.14159265358979,
MAX => 100,
};
Constant-folded subs are zero-cost at runtime. They do require the prototype to be () exactly (no arguments accepted), and the body must be a single expression that the optimiser can reduce. Adding return defeats the inlining:
sub PI () { return 3.14159; } # NOT inlined: explicit return
What prototypes do not do#
They do not check argument counts at runtime. Calling a
($)sub with three arguments is a parse-time error, but if you bypass the parser (via&fn(...), code refs,goto &fn), there is no runtime check.They do not affect method calls. Method dispatch is resolved at runtime through
@ISA, by which point parsing is long over. Prototypes on methods are silently ignored.They do not affect calls through code references. The call site sees a scalar, not a name; no name, no prototype lookup.
They are not part of the sub’s identity.
\&named_subloses prototype knowledge.
When you should use prototypes#
You are writing a sub that looks like a built-in. Examples:
A list-eating function with a block argument (
(&@)):mygrep,myfirst,try { ... } catch { ... }.An “inplace” helper that mutates an array in place (
(\@)or(\@@)).A constant-like sub (
()for inlining).
That’s the list. If you’re not building a built-in look-alike, do not reach for prototypes — use a signature, or just unpack @_.
When you should not use prototypes#
For “type checking”. Prototypes are not a type system.
sub f ($) { ... }does not say “takes one scalar”; it says “takes one expression in scalar context”. Callingf(@arr)passes the count of@arr, not the array itself, with no warning.For run-time argument-count enforcement. Use signatures (which do enforce arity at runtime) or write the check by hand.
On methods. They are silently ignored.
Forward declarations and recursion#
A recursive call sees the prototype only if the sub was predeclared:
sub fact ($); # forward declaration with prototype
sub fact ($) {
my ($n) = @_;
return $n <= 1 ? 1 : $n * fact($n - 1); # parsed with ($) in mind
}
Without the forward declaration, the recursive fact($n - 1) inside the body is parsed before the ($) prototype is finalised, so the prototype does not apply to that call.
Inspecting a prototype at runtime#
prototype returns a sub’s prototype string (or undef if there is none):
prototype(\&push); # '\@@' — yes, even built-ins have one
prototype('CORE::push'); # '\@@'
prototype(\&my_sub); # whatever you declared
See also#
prototype— runtime introspection.Signatures — the modern alternative for arity and named-parameter handling. They coexist with prototypes but have different goals.
sub— the keyword’s perlfunc page.Declaration — where prototypes attach in the declaration syntax.
Attributes —
:prototype(...)is the attribute form.