perl.gg / hidden-gems

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

The & Prototype

2026-05-05

Ever wonder how map and grep accept a bare block without the sub keyword?
my @upper = map { uc $_ } @names;
No sub. No comma after the block. Just a naked block of code sitting there like it owns the place. Try that with your own function and Perl yells at you.

Unless you know the secret: the (&@) prototype.

sub apply (&@) { my $code = shift; my @out; for (@_) { push @out, $code->($_); } return @out; } my @shouted = apply { uc $_ } @names;
That compiles. That runs. Your function now accepts a bare block exactly like map does. The & in the prototype tells Perl "the first argument is a code block," and Perl parses the call site accordingly.

Part 1: HOW MAP AND GREP ACTUALLY WORK

Before we build our own, let's understand what's happening at the call site.
my @evens = grep { $_ % 2 == 0 } @numbers;
Perl sees grep, recognizes it as a built-in that takes a block and a list, and parses accordingly. The block { $_ % 2 == 0 } becomes an anonymous subroutine. The @numbers list follows without a comma separating it from the block.

This syntax is special. Built-ins get it for free because the Perl parser knows about them. Your subroutines don't get this treatment by default. If you wrote:

sub my_grep { # ... } my @evens = my_grep { $_ % 2 == 0 } @numbers; # SYNTAX ERROR
Perl would choke. It sees the block as a hash reference constructor, not a code block. Then it finds @numbers floating with no operator and gives up.

Prototypes fix this.

Part 2: THE (&@) PROTOTYPE

The prototype (&@) tells Perl two things:
& First argument is a code block (or coderef) @ Everything else is a list
When Perl sees a function call with this prototype, it knows to parse the first argument as a block. No sub keyword needed. No comma between the block and the list.
sub each_pair (&@) { my $code = shift; while (@_) { my ($a, $b) = splice(@_, 0, 2); $code->($a, $b); } } my @data = (name => 'Alice', age => 30, role => 'admin'); each_pair { say "$_[0] = $_[1]" } @data;
name = Alice age = 30 role = admin
The block { say "$_[0] = $_[1]" } is automatically wrapped into an anonymous sub. Inside the function, shift pulls it off @_ as a coderef. The rest of @_ is the list.

Part 3: BUILDING A CUSTOM ITERATOR

Let's build something genuinely useful. A retry function that runs a block up to N times until it succeeds:
sub retry (&@) { my $code = shift; my %opts = @_; my $times = $opts{times} // 3; my $delay = $opts{delay} // 1; my $verbose = $opts{verbose} // 0; for my $attempt (1 .. $times) { my $result = eval { $code->() }; return $result unless $@; say "Attempt $attempt failed: $@" if $verbose; select(undef, undef, undef, $delay) if $attempt < $times; } die "All $times attempts failed. Last error: $@\n"; }
Now you can write:
my $response = retry { fetch_url('https://api.example.com/data') } times => 5, delay => 2, verbose => 1;
That reads like English. "Retry this block 5 times with a 2-second delay." The bare block syntax makes the call site clean and obvious. Compare it to the alternative without prototypes:
my $response = retry(sub { fetch_url('https://api.example.com/data') }, times => 5, delay => 2, verbose => 1);
The sub keyword and extra comma add noise. The prototype version is what you'd expect from a built-in.

Part 4: THE $_ ALIASING TRICK

Inside map and grep, $_ is aliased to each element of the list. Your prototype functions can do the same thing:
sub transform (&@) { my $code = shift; my @results; for (@_) { push @results, $code->(); } return @results; } my @names = qw(alice bob carol); my @upper = transform { uc $_ } @names; say "@upper"; # ALICE BOB CAROL
The key is the for (@_) loop. It sets $_ to each element, and the block refers to $_ naturally. The coderef doesn't need to receive arguments. It just uses $_ like map does.

But there's a subtle difference. In map, $_ is aliased to the actual list elements, meaning modifications to $_ inside the block modify the original. Our version with for (@_) does the same, since for aliases $_ to the elements of @_:

my @words = qw(hello world); transform { $_ = uc $_ } @words; say "@words"; # HELLO WORLD (originals modified!)
If you want to prevent this, copy the list first:
sub safe_transform (&@) { my $code = shift; my @copy = @_; my @results; for (@copy) { push @results, $code->(); } return @results; }

Part 5: A BENCHMARK FUNCTION

Here's a practical one. Time how long a block takes to run:
sub benchmark (&;$) { my ($code, $label) = @_; $label //= 'block'; require Time::HiRes; my $start = Time::HiRes::time(); my @result = $code->(); my $elapsed = Time::HiRes::time() - $start; printf "%s: %.4fs\n", $label, $elapsed; return wantarray ? @result : $result[0]; }
The prototype (&;$) means: required code block, optional scalar. The semicolon separates required from optional arguments.
my $total = benchmark { sum_of_primes(1_000_000) } 'prime sum';
prime sum: 0.3421s
Clean. The block runs, gets timed, and the result passes through. You can use it anywhere you'd use the original expression, with timing as a side effect.

Part 6: WITH_TIMEOUT

Another pattern that benefits from bare blocks. Run code with a deadline:
sub with_timeout (&@) { my $code = shift; my %opts = @_; my $secs = $opts{seconds} // 10; my $result; eval { local $SIG{ALRM} = sub { die "Timed out after ${secs}s\n" }; alarm $secs; $result = $code->(); alarm 0; }; alarm 0; # safety net die $@ if $@; return $result; }
Usage:
my $data = with_timeout { slow_network_call() } seconds => 30;
Compare to the version without the prototype:
my $data = with_timeout(sub { slow_network_call() }, seconds => 30);
Both work. The prototype version just reads better.

Part 7: PASSING CODEREFS EXPLICITLY

The & prototype also accepts an explicit coderef. You don't have to use a bare block:
sub apply (&@) { my $code = shift; return map { $code->($_) } @_; } # bare block (thanks to prototype) my @doubled = apply { $_ * 2 } 1, 2, 3; # explicit coderef (also works) my $doubler = sub { $_ * 2 }; my @doubled = apply $doubler, 1, 2, 3; # named function reference (prefix with &) sub triple { $_ * 3 } my @tripled = apply \&triple, 1, 2, 3;
All three work. The prototype is flexible. It accepts a bare block, a scalar containing a coderef, or a \&name reference. The block form is just syntactic sugar.

Note the comma. When you pass a coderef variable or \&name, you need the comma between arguments. The bare block form omits it. This is the same behavior as map and grep.

Part 8: WHY BUILTINS DON'T NEED PROTOTYPES

Here's the thing. map, grep, sort, and eval don't use prototypes. They're built into the Perl parser. The parser has special rules for them.

You can check:

say prototype('CORE::map'); # undef say prototype('CORE::grep'); # undef say prototype('CORE::sort'); # undef
They return undef because they're not prototyped. They're hardcoded into the parser. Built-ins have syntax that no prototype can fully replicate.

For example, sort can accept either a block or a function name:

sort { $a <=> $b } @list; # block sort by_name @list; # function name (no & or \&)
You can't do that second form with prototypes. Prototypes let you get close to built-in syntax, but not all the way. The (&@) prototype covers the most useful case: bare blocks before a list.

Part 9: GOTCHAS AND LIMITATIONS

Prototypes only work on named functions. Method calls and $coderef->() calls ignore prototypes entirely:
$obj->apply { ... } @list; # prototype ignored, syntax error
Prototypes are checked at compile time. If you call the function through a reference, the prototype doesn't apply:
my $fn = \&apply; $fn->({ ... }, @list); # no prototype magic, needs sub {}
The block must come first. The & must be the first character in the prototype. You can't put it in the middle:
sub broken ($&@) { ... } # & after $ - bare block WON'T work
Well, technically it compiles, but callers can't use bare block syntax. The & only triggers bare-block parsing when it's the first thing Perl sees.

Prototypes are controversial. Many experienced Perl programmers avoid them entirely. They can make code behave in surprising ways, especially ($) which forces scalar context on the argument. The (&@) prototype is one of the few that most people agree is genuinely useful.

Part 10: PUTTING IT ALL TOGETHER

Here's a small library of prototype-powered utility functions:
sub each_line (&@) { my $code = shift; for my $file (@_) { open my $fh, '<', $file or die "Cannot open $file: $!\n"; while (<$fh>) { chomp; $code->($_); } close $fh; } } sub with_lock (&$) { my ($code, $lockfile) = @_; require Fcntl; open my $fh, '>', $lockfile or die "Cannot create $lockfile: $!\n"; flock($fh, Fcntl::LOCK_EX()) or die "Cannot lock $lockfile: $!\n"; my $result = eval { $code->() }; my $err = $@; flock($fh, Fcntl::LOCK_UN()); close $fh; die $err if $err; return $result; } sub timed (&) { my $code = shift; require Time::HiRes; my $t0 = Time::HiRes::time(); $code->(); return Time::HiRes::time() - $t0; }
Usage:
# process all log files, line by line each_line { say if m~ERROR~ } glob('/var/log/*.log'); # run something under an exclusive lock with_lock { update_shared_state() } '/tmp/myapp.lock'; # measure elapsed time my $secs = timed { expensive_computation() }; say "Took ${secs}s";
.--. |o_o | "Look ma, no sub keyword." |:_/ | // \ \ (| | ) /'\_ _/`\ \___)=(___/ (&@) (&;$) (&) | | | v v v block block block +list +opt alone Your functions, built-in syntax.
The (&@) prototype is one of the few places where Perl prototypes genuinely improve code. It bridges the gap between built-in syntax and user-defined functions. Your retry, benchmark, with_timeout, and with_lock functions read like language features, not library calls.

Most of the time, prototypes are a footgun. But for the block-first pattern, they're exactly right. If you're writing utility functions that take a chunk of code as their first argument, reach for (&@). Your call sites will thank you.

perl.gg