perl.gg / hidden-gems

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

Slurp a List Tail into a Hash

2026-04-12

Assign a scalar and a hash from the same list:
my ($name, %attrs) = ('web01', port => 8080, role => 'frontend');
$name gets 'web01'. Everything else becomes key-value pairs in %attrs. One statement. No intermediate variables. No slicing. Just list assignment doing what it was born to do.

This works because Perl's list assignment feeds elements left to right. Scalars take one element each. A hash at the end of the list inhales whatever remains, pairing them off into keys and values.

You've probably used my ($first, @rest) = @list before. Same idea, but with a hash instead of an array. The hash swallows the tail of the list as pairs instead of individual elements. It's destructuring with benefits.

Part 1: THE BASIC MECHANICS

When Perl sees a list assignment, it works left to right on the targets:
my ($a, $b, %h) = (1, 2, x => 10, y => 20);
Here is what happens:
STEP TARGET GETS ---- ------ ---- 1 $a 1 2 $b 2 3 %h x => 10, y => 20 (the rest)
The two scalars each consume one element. The hash eats the remaining four elements as two key-value pairs. Clean and predictable.

The fat comma (=>) is just a fancy comma that auto-quotes its left side. So x => 10, y => 20 is really 'x', 10, 'y', 20. Four elements. Two pairs. One hash.

Part 2: THE FUNCTION ARGUMENT PATTERN

This is where it gets practical. Perl functions receive arguments in @_, a flat list. You can split positional arguments from named options in one shot:
sub deploy { my ($server, %opts) = @_; my $port = $opts{port} // 8080; my $workers = $opts{workers} // 4; my $env = $opts{env} // 'production'; say "Deploying to $server:$port ($env, $workers workers)"; } deploy('web01', port => 9090, env => 'staging'); deploy('db01'); # all defaults
The first argument is the server name. Everything after that is key-value pairs that land in %opts. Callers can pass any combination of options, in any order, or none at all. The // operator fills in defaults for anything not specified.

This pattern is everywhere in real Perl code. Mojolicious uses it. DBI uses it. Half of CPAN uses it. One positional argument, then named options. Simple and flexible.

Part 3: MULTIPLE SCALARS BEFORE THE HASH

You can have as many scalars as you want before the hash:
sub connect_db { my ($host, $port, %opts) = @_; my $user = $opts{user} // 'root'; my $timeout = $opts{timeout} // 30; my $ssl = $opts{ssl} // 1; say "Connecting to $host:$port as $user (timeout=${timeout}s, ssl=$ssl)"; } connect_db('db.example.com', 5432, user => 'app', ssl => 0);
Two positional arguments, then the named options. The scalars peel off the front of the list, and the hash gets the rest. Works perfectly as long as the remaining elements come in pairs.

Part 4: HOW THE "REST" REALLY WORKS

This is the part that trips people up. The hash doesn't magically know where it starts. It just gets whatever the scalars didn't take.
my ($first, %h) = (1, 2, 3, 4, 5);
$first gets 1. The hash gets (2, 3, 4, 5), which becomes 2 => 3, 4 => 5. Two pairs. Keys are 2 and 4.

But what about this:

my ($first, %h) = (1, 2, 3, 4, 5, 6, 7);
$first gets 1. The hash gets (2, 3, 4, 5, 6, 7). Three pairs: 2 => 3, 4 => 5, 6 => 7. Still fine.

The hash consumes elements two at a time. As long as the leftover count is even, everything is clean. If it's odd, you get a warning.

Part 5: THE ODD-ELEMENT GOTCHA

This is the big one. If the tail has an odd number of elements, Perl warns you:
my ($name, %h) = ('web01', 'port', 8080, 'orphan'); # Odd number of elements in hash assignment
The hash tries to pair up 'port' => 8080, 'orphan' => undef. Perl does it, but it complains. And rightly so. An odd tail usually means a bug. Maybe a missing value, a stray argument, or a caller who forgot a key.

Always make sure the elements after your scalars come in pairs. If they don't, your data is corrupt and you'll spend twenty minutes staring at Dumper output wondering why your hash looks wrong.

You can catch this at runtime:

sub safe_init { my ($name, @rest) = @_; if (@rest % 2) { die "Odd number of option elements for '$name'\n"; } my %opts = @rest; # proceed with %opts }
Grab the tail into an array first, check its length, then convert to a hash. Defensive, but sometimes necessary for public-facing APIs.

Part 6: COMBINING WITH ARRAY SLURP

You might wonder: can I slurp into an array AND a hash? Short answer: no. Only the last aggregate wins.
my ($x, @arr, %h) = (1, 2, 3, 4, 5); # @arr gets (2, 3, 4, 5) - the array eats everything # %h is empty
The array is greedy. It takes all remaining elements, leaving nothing for the hash. Perl doesn't split the remainder between them.

This is the rule: one aggregate at the end. Either an array or a hash, never both. Scalars can come before it, but only one slurpy thing at the tail.

WORKS: my ($a, $b, @rest) = @list; # array slurp WORKS: my ($a, $b, %opts) = @list; # hash slurp BROKEN: my ($a, @rest, %h) = @list; # array eats everything BROKEN: my (%h, @rest) = @list; # hash eats everything
If you need both, slurp into an array and then split it yourself:
my ($name, @rest) = @_; my @positional = splice(@rest, 0, 3); # take first 3 my %opts = @rest; # rest becomes hash

Part 7: THE @_ PATTERN IN DEPTH

The most common use of hash slurping is in subroutine signatures. Here are the three classic patterns:

Pattern 1: All named arguments

sub configure { my (%args) = @_; # everything is key-value } configure(host => 'localhost', port => 3000);
Pattern 2: One positional, rest named
sub process_file { my ($file, %opts) = @_; # $file is required, %opts is optional } process_file('/var/log/app.log', verbose => 1, limit => 100);
Pattern 3: Multiple positional, rest named
sub copy_file { my ($src, $dst, %opts) = @_; # two required, rest optional } copy_file('/tmp/a.txt', '/tmp/b.txt', overwrite => 1, backup => 1);
Each pattern uses the same mechanism. Scalars peel off the front, hash gets the rest. The only difference is how many scalars you declare.

Part 8: HASHREF VS HASH SLURP

Some codebases prefer passing a hashref instead of a flat hash:
# hashref style deploy('web01', { port => 9090, env => 'staging' }); sub deploy { my ($server, $opts) = @_; my $port = $opts->{port} // 8080; }
Versus the flat hash style:
# flat hash style deploy('web01', port => 9090, env => 'staging'); sub deploy { my ($server, %opts) = @_; my $port = $opts{port} // 8080; }
The flat style is fewer characters at the call site. No braces, no arrow operator. The hashref style is explicit about where the options start. Both work. Both are idiomatic. Pick one and be consistent.

The flat style has one advantage: callers can build the option list dynamically.

my @extra; push @extra, (verbose => 1) if $debug; push @extra, (timeout => 60) if $slow_network; deploy('web01', port => 9090, @extra);
The list flattens naturally. With a hashref, you'd need to build the hash first, then pass a reference to it. More steps for the same result.

Part 9: REAL-WORLD EXAMPLE

Here is a complete script that uses the pattern for a simple HTTP health checker:
#!/usr/bin/env perl use strict; use warnings; use feature 'say'; sub check_host { my ($host, %opts) = @_; my $port = $opts{port} // 80; my $path = $opts{path} // '/'; my $timeout = $opts{timeout} // 5; my $expect = $opts{expect} // 200; my $url = "http://$host:$port$path"; my $result = `curl -s -o /dev/null -w '%{http_code}' --max-time $timeout '$url' 2>&1`; chomp $result; my $status = ($result == $expect) ? 'OK' : 'FAIL'; say sprintf("%-6s %s (got %s, expected %s)", $status, $url, $result, $expect); return $result == $expect; } # clean call site, easy to read check_host('web01.local', port => 8080, path => '/health'); check_host('api.local', port => 3000, path => '/ping', timeout => 2); check_host('db.local', port => 5432, expect => 000); # just check port is open
One positional argument (the host), then named options for everything else. The caller only specifies what they care about. Defaults handle the rest. This is the pattern at work.

Part 10: THE TAKEAWAY

The hash slurp is not a trick. It's fundamental Perl list mechanics. Scalars eat one element each. A hash at the end eats the rest in pairs. That's it.
@_ = ('web01', port => 8080, role => 'frontend') | \________________________/ v v $name %attrs 'web01' { port => 8080, role => 'frontend' } .--. .--. .--. .--. .--. | 1|| 2|| 3|| 4|| 5| <- elements '--' '--' '--' '--' '--' | | | | | v \----v----/----/ $a %hash {2=>3, 4=>5} .--. |o_o | "First come, first served. |:_/ | The hash gets the leftovers." // \ \ (| | ) /'\_ _/`\ \___)=(___/
Use it for function arguments. Use it for config parsing. Use it anywhere you have a list with a fixed head and a variable key-value tail. It's clean, it's fast, and it's how Perl wants you to work with mixed data.

perl.gg