<!-- category: hidden-gems -->
The & Prototype
Ever wonder howmap and grep accept a bare block without the sub keyword?
Nomy @upper = map { uc $_ } @names;
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.
That compiles. That runs. Your function now accepts a bare block exactly likesub apply (&@) { my $code = shift; my @out; for (@_) { push @out, $code->($_); } return @out; } my @shouted = apply { uc $_ } @names;
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.Perl seesmy @evens = grep { $_ % 2 == 0 } @numbers;
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:
Perl would choke. It sees the block as a hash reference constructor, not a code block. Then it findssub my_grep { # ... } my @evens = my_grep { $_ % 2 == 0 } @numbers; # SYNTAX ERROR
@numbers floating with no operator and gives up.
Prototypes fix this.
Part 2: THE (&@) PROTOTYPE
The prototype(&@) tells Perl two things:
When Perl sees a function call with this prototype, it knows to parse the first argument as a block. No& First argument is a code block (or coderef) @ Everything else is a list
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;
The blockname = Alice age = 30 role = admin
{ 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. Aretry function that runs a block up to N times until it succeeds:
Now you can write: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"; }
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 { fetch_url('https://api.example.com/data') } times => 5, delay => 2, verbose => 1;
Themy $response = retry(sub { fetch_url('https://api.example.com/data') }, times => 5, delay => 2, verbose => 1);
sub keyword and extra comma add noise. The prototype version is what you'd expect from a built-in.
Part 4: THE $_ ALIASING TRICK
Insidemap and grep, $_ is aliased to each element of the list. Your prototype functions can do the same thing:
The key is thesub 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
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 @_:
If you want to prevent this, copy the list first:my @words = qw(hello world); transform { $_ = uc $_ } @words; say "@words"; # HELLO WORLD (originals modified!)
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:The prototypesub 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]; }
(&;$) means: required code block, optional scalar. The semicolon separates required from optional arguments.
my $total = benchmark { sum_of_primes(1_000_000) } 'prime sum';
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.prime sum: 0.3421s
Part 6: WITH_TIMEOUT
Another pattern that benefits from bare blocks. Run code with a deadline:Usage: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; }
Compare to the version without the prototype:my $data = with_timeout { slow_network_call() } seconds => 30;
Both work. The prototype version just reads better.my $data = with_timeout(sub { slow_network_call() }, seconds => 30);
Part 7: PASSING CODEREFS EXPLICITLY
The& prototype also accepts an explicit coderef. You don't have to use a bare block:
All three work. The prototype is flexible. It accepts a bare block, a scalar containing a coderef, or asub 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;
\&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:
They returnsay prototype('CORE::map'); # undef say prototype('CORE::grep'); # undef say prototype('CORE::sort'); # undef
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:
You can't do that second form with prototypes. Prototypes let you get close to built-in syntax, but not all the way. Thesort { $a <=> $b } @list; # block sort by_name @list; # function name (no & or \&)
(&@) 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:
Prototypes are checked at compile time. If you call the function through a reference, the prototype doesn't apply:$obj->apply { ... } @list; # prototype ignored, syntax error
The block must come first. Themy $fn = \&apply; $fn->({ ... }, @list); # no prototype magic, needs sub {}
& must be the first character in the prototype. You can't put it in the middle:
Well, technically it compiles, but callers can't use bare block syntax. Thesub broken ($&@) { ... } # & after $ - bare block WON'T work
& 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:Usage: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; }
# 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";
The.--. |o_o | "Look ma, no sub keyword." |:_/ | // \ \ (| | ) /'\_ _/`\ \___)=(___/ (&@) (&;$) (&) | | | v v v block block block +list +opt alone Your functions, built-in syntax.
(&@) 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