Perl Subroutine Signatures: Cleaner Parameters Since 5.36
Perl 5.36 made subroutine signatures stable. They replace manual @_ unpacking with declared parameters, default values, and runtime arity checks — cutting boilerplate and catching call-site errors early. A worked config-loader example shows slurpy hashes and lazy defaults, plus the trade-offs.
08 Sept 2025, 15:02 UTC

The Problem with Manual @_ Unpacking
For decades, Perl subroutines pulled arguments from the magical @_ array. A typical function started with a line like my ($self, $name, $opts) = @_; or a cascade of shift calls. This works, but it hides the function's interface inside the body, makes default values verbose, and silently turns missing arguments into undef — often surfacing as bugs far from the call site.
Since Perl 5.36 (released May 2022), subroutine signatures are stable and enabled by default with use v5.36. They let you declare parameters in the subroutine header, enforce arity at runtime, and express defaults inline. This post walks through what changes, a worked example, and the trade-offs you'll hit in real codebases.
What Signatures Replace
Consider the old style:
sub greet {
my ($name, $greeting) = @_;
$greeting //= 'hello';
print "$greeting, $name!\n";
}
With signatures, the same function becomes:
use v5.36;
sub greet ($name, $greeting = 'hello') {
say "$greeting, $name!";
}
The differences:
- Parameter list in the signature — callers and readers see the expected arguments immediately.
- Default values evaluated per call —
$greeting = 'hello'runs each invocation, so you can write$greeting = uc $nameor reference earlier parameters. - Arity enforcement — calling
greet()orgreet('Ada', 'hi', 'extra')throws"Too few arguments for subroutine"or"Too many arguments"at runtime, instead of silently assigningundefor ignoring extras. sayand modern defaults included —use v5.36enables the feature bundle that includes signatures,say, andstate.
Worked Example: A Config Loader with Slurpy Options
Here's a practical subroutine that reads a YAML config file, merges caller-provided overrides, and returns a hashref. It demonstrates required parameters, a default value, and a slurpy hash for flexible options.
use v5.36;
use YAML::XS qw(LoadFile);
sub load_config (
$path,
$schema_version = 1,
%overrides # slurpy hash — collects remaining key/value pairs
) {
my $data = LoadFile($path);
die "Config schema mismatch: expected v$schema_version"
unless ($data->{schema_version} // 1) == $schema_version;
# Merge overrides (shallow). Later keys win.
@{$data}{keys %overrides} = values %overrides;
return $data;
}
# Usage:
my $cfg = load_config(
'app.yaml',
2,
debug => 1,
cache_dir => '/tmp/myapp',
);
Where to run this: Any system with Perl 5.36+ and YAML::XS installed (cpanm YAML::XS). No special permissions needed beyond read access to the config file.
What it shows:
$schema_version = 1is a lazy default; if you wrote$schema_version = compute_default(), that sub runs on every call that omits the second argument. Defaults can also reference earlier parameters, e.g.sub f ($a, $b = $a * 2).%overridesslurps all remaining named arguments. Callers passdebug => 1and it arrives as a plain hash — with no compile-time typo checking on keys.- Arity is enforced for the positional parameters; you must provide at least
$path.
Trade-offs and Limitations
Signatures are deliberately minimal. They solve parameter declaration and count checking, but they are not a type system.
- No type constraints.
sub f (Int $x)is not valid Perl. If you needInt,ArrayRef, or custom checks, addType::Params,Params::Validate, or manual validation inside the body. - Slurpy hash loses typo safety.
load_config('x', 1, deubg => 1)silently creates$overrides{deubg}. Teams often pair signatures with a validation step:my %valid = (debug => 1, cache_dir => 1); die "Unknown option $_" for grep !$valid{$_}, keys %overrides;. - Prototypes are incompatible. You cannot combine a signature with a prototype on the same sub. Prototypes are a separate, largely discouraged mechanism; signatures replace them for parameter handling.
- Version floor. On Perl 5.20–5.34, you need
use feature 'signatures'; no warnings 'experimental::signatures';and behavior differed slightly across releases. For code that must run on 5.34 or older, either keep the pragma boilerplate or stick to manual@_unpacking.
Quick Verification Checklist
Before committing signature-based code to a shared repo:
- Run
perl -vand confirm the interpreter is 5.36 or newer. - Test a one-liner:
perl -M5.036 -e 'sub add ($a, $b) { $a + $b } print add(2,3)'— should print5with no warnings. - Call a signatured sub with wrong arity:
perl -M5.036 -e 'sub f ($x) {} f()'— expect a"Too few arguments for subroutine"runtime error. - Check your project's minimum Perl version in
Makefile.PL,cpanfile, or CI config. If it's below 5.36, decide whether to add the experimental pragmas or avoid signatures in that code path.
Closing: Adopt Incrementally
You don't need a flag-day rewrite. Start by writing new subroutines with signatures where the codebase already requires 5.36+. Convert hot-path functions that suffer from argument-count bugs first — the runtime arity check catches caller mistakes immediately. Keep a validation module handy for complex option hashes, and document the version requirement in your contributor guide. The syntax is small, but the reduction in boilerplate and silent undef bugs adds up fast.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.