<!-- category: hidden-gems -->
Slurp a List Tail into a Hash
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:Here is what happens:my ($a, $b, %h) = (1, 2, x => 10, y => 20);
The two scalars each consume one element. The hash eats the remaining four elements as two key-value pairs. Clean and predictable.STEP TARGET GETS ---- ------ ---- 1 $a 1 2 $b 2 3 %h x => 10, y => 20 (the rest)
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:
The first argument is the server name. Everything after that is key-value pairs that land insub 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
%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: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.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);
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:The hash tries to pair upmy ($name, %h) = ('web01', 'port', 8080, 'orphan'); # Odd number of elements in hash assignment
'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:
Grab the tail into an array first, check its length, then convert to a hash. Defensive, but sometimes necessary for public-facing APIs.sub safe_init { my ($name, @rest) = @_; if (@rest % 2) { die "Odd number of option elements for '$name'\n"; } my %opts = @rest; # proceed with %opts }
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.The array is greedy. It takes all remaining elements, leaving nothing for the hash. Perl doesn't split the remainder between them.my ($x, @arr, %h) = (1, 2, 3, 4, 5); # @arr gets (2, 3, 4, 5) - the array eats everything # %h is empty
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.
If you need both, slurp into an array and then split it yourself: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
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
Pattern 2: One positional, rest namedsub configure { my (%args) = @_; # everything is key-value } configure(host => 'localhost', port => 3000);
Pattern 3: Multiple positional, rest namedsub process_file { my ($file, %opts) = @_; # $file is required, %opts is optional } process_file('/var/log/app.log', verbose => 1, limit => 100);
Each pattern uses the same mechanism. Scalars peel off the front, hash gets the rest. The only difference is how many scalars you declare.sub copy_file { my ($src, $dst, %opts) = @_; # two required, rest optional } copy_file('/tmp/a.txt', '/tmp/b.txt', overwrite => 1, backup => 1);
Part 8: HASHREF VS HASH SLURP
Some codebases prefer passing a hashref instead of a flat hash:Versus the flat hash style:# hashref 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.# flat hash style deploy('web01', port => 9090, env => 'staging'); sub deploy { my ($server, %opts) = @_; my $port = $opts{port} // 8080; }
The flat style has one advantage: callers can build the option list dynamically.
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.my @extra; push @extra, (verbose => 1) if $debug; push @extra, (timeout => 60) if $slow_network; deploy('web01', port => 9090, @extra);
Part 9: REAL-WORLD EXAMPLE
Here is a complete script that uses the pattern for a simple HTTP health checker: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.#!/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
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.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.@_ = ('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." // \ \ (| | ) /'\_ _/`\ \___)=(___/
perl.gg