perl.gg / hidden-gems

<!-- category: hidden-gems -->

The \@ Prototype: Auto-Referencing Magic

2026-04-06

You write a function that takes two arrays. You call it like this:
compare(@left, @right);
Inside the function, both arrays have been flattened into one big @_. You have no idea where @left ends and @right begins. The arrays merged. Your data is soup.

Now add a prototype:

sub compare (\@\@) { my ($left_ref, $right_ref) = @_; # $left_ref and $right_ref are arrayrefs # the caller wrote compare(@left, @right) # Perl silently converted both to references }
The \@ prototype tells Perl: "when the caller passes an array, silently take a reference to it before the function sees it." The caller writes natural array syntax. The function receives clean references. No explicit backslash-reference at the call site. No flattening. No soup.

Part 1: WHAT PROTOTYPES ACTUALLY DO

Prototypes in Perl are parsing hints. They tell the Perl parser how to interpret the arguments at the call site at compile time. They do not check types. They do not enforce contracts. They modify how the parser processes the argument list.
sub mysub ($) { } # expect one scalar sub mysub (@) { } # expect a list sub mysub (&) { } # expect a code block sub mysub (\@) { } # expect an array, auto-reference it
When you call mysub(@array) and the prototype is (\@), Perl does not pass @array flattened into a list. Instead, it silently rewrites the call to mysub(\@array) at compile time. The function receives an array reference.

This is the critical distinction. Prototypes happen at parse time, not at runtime. They change how the call looks before the function ever executes.

Part 2: THE \@ AND \% PROTOTYPES

The \ prefix before a sigil means "take a reference to this argument automatically":
PROTOTYPE CALLER WRITES FUNCTION RECEIVES --------- ------------- ----------------- \@ func(@array) func(\@array) # arrayref \% func(%hash) func(\%hash) # hashref \$ func($scalar) func(\$scalar) # scalarref \& func(&code) func(\&code) # coderef
The caller does not know references are involved. They write the natural @array or %hash syntax. Perl handles the conversion invisibly.
sub dump_array (\@) { my ($aref) = @_; for my $item (@$aref) { say " - $item"; } } my @fruits = ('apple', 'banana', 'cherry'); dump_array(@fruits); # caller writes @fruits, not \@fruits
- apple - banana - cherry
The function receives an arrayref. It dereferences with @$aref. The caller has no idea a reference was created.

Part 3: MULTIPLE ARRAYS WITHOUT FLATTENING

This is the practical payoff. Pass two (or more) arrays to a function and keep them separate:
sub intersect (\@\@) { my ($a_ref, $b_ref) = @_; my %seen; $seen{$_}++ for @$a_ref; my @common; for my $item (@$b_ref) { push @common, $item if $seen{$item}; } return @common; } my @list1 = (1, 2, 3, 4, 5); my @list2 = (3, 4, 5, 6, 7); my @shared = intersect(@list1, @list2); say join(', ', @shared); # 3, 4, 5
Without the prototype, intersect(@list1, @list2) would flatten both arrays into @_ as (1, 2, 3, 4, 5, 3, 4, 5, 6, 7). You would have no way to know which elements came from which array.

With (\@\@), each array is auto-referenced, and the function receives two distinct arrayrefs. Problem solved.

Part 4: WHAT HAPPENS WITHOUT PROTOTYPES

Let's see the alternative. Without prototypes, you force the caller to create references manually:
# no prototype version sub intersect_manual { my ($a_ref, $b_ref) = @_; # same implementation... } # caller must write backslashes my @shared = intersect_manual(\@list1, \@list2);
That \@list1 and \@list2 at the call site is not terrible. But it is extra syntax that the caller has to remember. And if they forget and write intersect_manual(@list1, @list2), they get silently wrong results instead of an error. The arrays flatten and the function does garbage.

The prototype catches this mistake at compile time:

Type of arg 1 to main::intersect must be array (not list)
The prototype enforces that the caller passes a named array, not an arbitrary list. It is not type safety in the strict sense, but it is better than nothing.

Part 5: THE \% HASH PROTOTYPE

Same trick for hashes:
sub print_hash (\%) { my ($href) = @_; for my $key (sort keys %$href) { say " $key = $href->{$key}"; } } my %config = ( host => 'localhost', port => 8080, debug => 1, ); print_hash(%config); # auto-referenced!
And mixing arrays and hashes:
sub transform (\@\%) { my ($list_ref, $map_ref) = @_; return map { $map_ref->{$_} // $_ } @$list_ref; } my @keys = ('a', 'b', 'c', 'd'); my %lookup = (a => 'alpha', b => 'bravo', c => 'charlie'); my @result = transform(@keys, %lookup); say join(', ', @result); # alpha, bravo, charlie, d
The caller writes transform(@keys, %lookup) with no references in sight. The function receives ($arrayref, $hashref). Clean on both sides.

Part 6: THE \& PROTOTYPE AND BARE BLOCKS

The \& prototype is not actually the interesting one for code references. The & prototype (without the backslash) is the magic one. It lets the caller pass a bare block without the sub keyword:
sub with_timer (&) { my ($code) = @_; my $start = time; $code->(); my $elapsed = time - $start; say "Took ${elapsed}s"; } # caller writes a bare block, no 'sub' keyword needed with_timer { sleep 1; say "doing work..."; };
This is how sort { $a <=> $b } works. The & prototype tells Perl to accept a bare block as the first argument and wrap it in an anonymous sub.

Without the prototype, the caller would have to write:

with_timer(sub { sleep 1; say "doing work..."; });
The prototype eliminates the sub and the outer parentheses. The result looks like a built-in control structure.

Part 7: COMBINING PROTOTYPES

You can mix prototype characters for functions with multiple arguments of different types:
sub apply_to_each (&\@) { my ($code, $array_ref) = @_; for my $item (@$array_ref) { $code->($item); } } my @names = ('Alice', 'Bob', 'Charlie'); apply_to_each { say "Hello, $_[0]!" } @names;
Hello, Alice! Hello, Bob! Hello, Charlie!
The & grabs the bare block as a coderef. The \@ auto- references the array. The caller writes a natural, readable call. The function receives clean, typed arguments.

Another combo, a function that takes a scalar predicate and an array:

sub count_matching ($\@) { my ($pattern, $array_ref) = @_; my $count = 0; for my $item (@$array_ref) { $count++ if $item =~ m~$pattern~; } return $count; } my @logs = ('ERROR: disk full', 'INFO: started', 'ERROR: timeout'); my $errors = count_matching('ERROR', @logs); say "$errors errors found"; # 2 errors found

Part 8: WHY PROTOTYPES ARE CONTROVERSIAL

Prototypes have a bad reputation in the Perl community. Here is why:

They do not work on method calls. Prototypes are ignored when a function is called as a method ($obj->method(@args)). This means OO code gets zero benefit.

They do not work with &func() calls. If someone calls your function with the explicit & sigil or through a reference, prototypes are bypassed:

sub safe_div (\$\$) { my ($a_ref, $b_ref) = @_; return $$b_ref == 0 ? undef : $$a_ref / $$b_ref; } safe_div($x, $y); # prototype active, auto-references &safe_div($x, $y); # prototype BYPASSED, $x and $y passed as-is
They give a false sense of type safety. A (\@) prototype ensures the caller passes a named array, but it says nothing about what is inside that array. It is a parsing hint, not a type system.

They can confuse readers who are not familiar with them. The call site looks like func(@array), which normally means "flatten the array." With a prototype, it secretly means "reference the array." Hidden behavior is sometimes surprising.

Part 9: WHEN TO USE PROTOTYPES

Despite the controversy, there are clear winning use cases:
USE PROTOTYPES WHEN: * You need multiple array/hash arguments without flattening * You're building DSL-like syntax with bare blocks * You want compile-time argument count checking * You're writing utility functions (not methods) SKIP PROTOTYPES WHEN: * Writing OO code (prototypes are ignored on methods) * The function is called through references * Simple functions where \@ at call site is fine * Team does not understand prototypes
The \@ and \% auto-referencing prototypes are the most universally useful. They solve a real problem (array flattening) with zero cognitive overhead at the call site.

The & prototype for bare blocks is great for building map/grep/sort-like constructs.

Everything else is niche.

Part 10: THE MODERN ALTERNATIVE

Perl 5.20 introduced subroutine signatures, which handle argument unpacking without prototypes:
use feature 'signatures'; sub greet ($name, $greeting = "Hello") { say "$greeting, $name!"; }
But signatures do not auto-reference. If you need a function that takes two separate arrays, signatures alone will not help. You still have to choose: explicit references at the call site, or prototypes for auto-referencing.
# with signatures (caller must pass refs) use feature 'signatures'; sub intersect_sig ($a_ref, $b_ref) { ... } intersect_sig(\@left, \@right); # with prototype (caller passes arrays naturally) sub intersect_proto (\@\@) { ... } intersect_proto(@left, @right);
Both work. The prototype version has a nicer call site. The signature version is more explicit. Pick the one that fits your codebase.
Caller writes: Function receives: +-----------+ +---------------+ | @array | ---> | $array_ref | +-----------+ +---------------+ | \@ prototype does this silently .--. |o_o | "You wrote @array. |:_/ | I heard \\@array. // \ \ Same thing, right?" (| | ) /'\_ _/`\ \___)=(___/
The \@ prototype is not deep magic. It is a small, practical feature that solves one of Perl's most common annoyances: the flattening of arrays when passed to functions. One character in the prototype, and the problem disappears.

Your caller writes clean, natural Perl. Your function receives clean references. Nobody writes a backslash. Nobody remembers to dereference at the call site. The prototype handles the translation, and both sides of the API are better for it.

That is what good language design looks like. Not a grand type system. Just a tiny, targeted feature that makes the common case effortless.

perl.gg