Perl Part I · Module 7 · References
Hashes of Arrays
If you learn one nested structure properly, make it this one. A hash whose values are array references holds a named list per key — ports per block, violations per corner, lines per file — and it is the shape that almost every log-processing script converges on. The reason it is worth singling out is that building one takes a single line with no setup: push @{ $h{$key} }, $value creates the inner array the first time it is needed and appends to it every time after, with no declaration and no check for whether the key already exists. That economy is the whole appeal. The cost is one trap, which is that a key holding an empty list still tests as true.
Intermediate16 min readPerlPart IReferencesData Structures
Part I · Module 7 · Chapter 7.7 · Hashes of Arrays
1. One push, No Setup
use strict;
use warnings;
my %ports;
push @{ $ports{alu} }, "clk", "rst", "a", "b";
push @{ $ports{fifo} }, "clk", "wr_en";
for my $blk (sort keys %ports) {
printf "%-5s %d port(s) : %s\n",
$blk, scalar(@{ $ports{$blk} }), join(" ", @{ $ports{$blk} });
}alu 4 port(s) : clk rst a b
fifo 2 port(s) : clk wr_enLook at what is not in that program. %ports is declared empty. The inner arrays are never declared. There is no if (exists $ports{alu}) and no $ports{alu} = [] initialisation. The two push lines do all of it.
push @{ $ports{alu} }, ... reads as: open the array referred to by $ports{alu}, and append to it. The first time, $ports{alu} does not exist — and Perl creates it, as an array reference, because the surrounding @{ } in an assignable position says that is what has to be there. That is autovivification, and Chapter 7.9 is the chapter on it. For now the useful fact is simply that it works and that you can rely on it.
Reading it back is Chapter 7.4 §4's combination again — navigate to the reference, then @{ } to open it:
| Question | Expression |
|---|---|
| the list for one key | @{ $ports{alu} } |
| how many items that key has | scalar(@{ $ports{alu} }) |
| one item | $ports{alu}[0] |
| its last index | $#{ $ports{alu} } |
| every key | keys %ports |
| append to one key's list | push @{ $ports{alu} }, $x |
Note $ports{alu}[0] for a single item. No arrow is needed before [0] because it sits between two subscripts — {alu} then [0] — which is exactly Chapter 7.4 §3's rule.
2. The Grouping Idiom
This is the pattern in its natural habitat: a flat list of lines going in, organised data coming out.
use strict;
use warnings;
my @lines = (
"alu clk",
"alu rst",
"fifo clk",
"alu a",
"fifo wr_en",
);
my %by_block;
for my $line (@lines) {
my ($blk, $port) = split ' ', $line;
push @{ $by_block{$blk} }, $port;
}
for my $blk (sort keys %by_block) {
printf "%-5s (%d) : %s\n",
$blk, scalar(@{ $by_block{$blk} }), join(", ", @{ $by_block{$blk} });
}
printf "blocks seen : %d\n", scalar(keys %by_block);alu (3) : clk, rst, a
fifo (2) : clk, wr_en
blocks seen : 2The input interleaves alu and fifo lines, and the grouping does not care — each line goes to its own key regardless of order. That is the property that makes this better than sorting the input first and tracking when the key changes, which is the approach people reach for when they do not know this idiom.
Three details worth noticing:
- The loop body is one
splitand onepush. There is no bookkeeping. sort keys %by_blockgives deterministic output. Without thesortthe block order would vary between runs, as Chapter 6.5 established and Chapter 6.6 addressed. Any report you intend to diff needs thesort.- Within a key, order is preserved.
alureadsclk, rst, a— the order the lines arrived in. The outer container is a hash and has no order; the inner containers are arrays and keep theirs. That combination is often exactly what you want.
3. Empty Lists Are Still True
The trap, and it is the same one Chapter 7.5 §6 raised, arriving where it actually causes trouble.
use strict;
use warnings;
my %fail = ( alu => ["setup"], fifo => [] );
for my $blk (sort keys %fail) {
my $n = scalar(@{ $fail{$blk} });
printf "%-5s ref is %s, list is %s, count %d\n",
$blk,
($fail{$blk} ? "true" : "false"),
(@{ $fail{$blk} } ? "true" : "false"),
$n;
}alu ref is true, list is true, count 1
fifo ref is true, list is false, count 0fifo has no failures, and $fail{fifo} is true. It holds an array reference, and Chapter 7.1 §6 established that every reference is true regardless of what it refers to.
So this is wrong:
print "BUGGY if (\$fail{\$blk}):\n";
for my $blk (sort keys %fail) {
if ($fail{$blk}) { print " $blk FAILED\n"; }
}
print "FIXED if (\@{ \$fail{\$blk} }):\n";
for my $blk (sort keys %fail) {
if (@{ $fail{$blk} }) { print " $blk FAILED\n"; }
}BUGGY if ($fail{$blk}):
alu FAILED
fifo FAILED
FIXED if (@{ $fail{$blk} }):
alu FAILEDif ($h{$k}) asks whether the key holds a reference. if (@{ $h{$k} }) asks whether the list has anything in it. The second is nearly always the question you mean. It is worth being deliberate here because the failure mode is a report that declares every block broken, which is the kind of bug that gets noticed late and trusted early.
4. Operating on the Inner Lists
Once opened, an inner list is an ordinary array, so everything from Module 5 applies.
use strict;
use warnings;
my %slack = (
alu => [-0.12, -0.40, 0.05],
fifo => [ 0.34, 0.11],
regf => [-0.05],
);
for my $blk (sort keys %slack) {
my @v = @{ $slack{$blk} };
my ($worst) = sort { $a <=> $b } @v;
my $neg = grep { $_ < 0 } @v;
my $sum = 0;
$sum += $_ for @v;
printf "%-5s n=%d worst=%+.2f negative=%d mean=%+.3f\n",
$blk, scalar(@v), $worst, $neg, $sum / scalar(@v);
}
my $total = 0;
$total += scalar(@{ $slack{$_} }) for keys %slack;
printf "total paths across all blocks : %d\n", $total;alu n=3 worst=-0.40 negative=2 mean=-0.157
fifo n=2 worst=+0.11 negative=0 mean=+0.225
regf n=1 worst=-0.05 negative=1 mean=-0.050
total paths across all blocks : 6Two idioms in there are worth pointing out:
my $neg = grep { ... } @v;—grepin scalar context returns the count of matches, not the matches. That is Chapter 5.10 §5's behaviour and it saves ascalar(my @tmp = grep ...).$total += scalar(@{ $slack{$_} }) for keys %slack;— summing across the inner lists without caring about key order, so nosortis needed. Usesortwhen you are printing, not when you are accumulating.
Copying the list out with my @v = @{ $slack{$blk} } is a style choice. It reads well when you use the list several times, and it is a copy — pushing to @v would not change the structure. When you intend to modify, work through the reference directly: push @{ $slack{$blk} }, -0.9.
5. Sorting by Something Inside the Lists
A frequent requirement: order the keys by a property of their lists rather than alphabetically.
use strict;
use warnings;
my %slack = (
alu => [-0.12, -0.40, 0.05],
fifo => [ 0.34, 0.11],
regf => [-0.05],
);
my @by_count = sort { @{ $slack{$b} } <=> @{ $slack{$a} } or $a cmp $b } keys %slack;
printf "most paths first : %s\n", join(" ", @by_count);
my %worst = map {
my ($w) = sort { $a <=> $b } @{ $slack{$_} };
($_ => $w);
} keys %slack;
my @by_worst = sort { $worst{$a} <=> $worst{$b} } keys %worst;
printf "worst slack first: %s\n", join(" ", @by_worst);
printf "and alu's worst is %+.2f\n", $worst{alu};most paths first : alu fifo regf
worst slack first: alu regf fifo
and alu's worst is -0.40@{ $slack{$b} } <=> @{ $slack{$a} } compares the two lists in scalar context, which is their lengths — the <=> operator forces it, so no scalar() is needed. The or $a cmp $b is a tiebreaker, which matters because Chapter 5.9 §7 noted that Perl's sort is not guaranteed stable; without it, two blocks with the same count could swap order between runs.
The map block builds a second hash of pre-computed worst values. Computing a derived value once per key and sorting on that is worth the extra lines whenever the computation is more than trivial, since a comparator runs many times per sort.
6. Why This Matters in Engineering Work
It is the shape that log processing produces. Any tool output where many lines share a key — a block, a corner, a file, a net — becomes a hash of arrays, and the loop that builds it is one line. Recognising that on sight is the difference between a ten-line parser and a forty-line one.
Order is kept where it matters and dropped where it does not. The keys have no order, which is fine because you sort them for output. The values inside each key keep the order they arrived in, which is often significant — the first violation reported, the sequence of events for one net.
And the empty-list check is a real correctness issue. A per-block failure list is the canonical use of this structure, and if ($fail{$blk}) is true for the clean blocks. That is a one-character class of bug — @{ } — with a very visible consequence.
7. Common Mistakes
if ($h{$k}) to test whether the list has anything in it. Always true if the key exists. Use if (@{ $h{$k} }).
push $h{$k}, $x — push needs an array. Write push @{ $h{$k} }, $x.
Initialising before pushing. $h{$k} = [] and if (exists $h{$k}) are both unnecessary.
Expecting keys you never pushed to. Autovivification creates only the keys you touch. Use $h{$_} //= [] for @all; when the report must list every one.
Forgetting sort on keys when printing. The block order will vary between runs.
Expecting a tie in a custom sort to be stable. Add an explicit tiebreaker.
Pushing to a copied-out list. my @v = @{ $h{$k} } is a copy; push @v, $x changes nothing in the structure.
8. Exercise
Exercise 1 — no setup. Declare an empty hash. With two push @{ ... } statements, give one key four values and another two. Print each key's count and contents, sorted by key. Confirm you wrote no initialisation and no exists check.
Exercise 2 — the access table. For one key, print the whole list, its count, its first element, and its last index. Note which of the four needed @{ } and which did not.
Exercise 3 — grouping. Take §2's five interleaved lines, split each, and group them. Print each key with its count and comma-joined values, plus the number of keys. Then reverse the order of the input lines and confirm only the within-key order changes.
Exercise 4 — empty but true. Build ( alu => ["setup"], fifo => [] ). For each key print whether the value is true and whether the dereferenced list is true. Then write the buggy if ($h{$k}) report and the correct if (@{ $h{$k} }) one, and show the different output.
Exercise 5 — keys that must appear. Build a failure hash by pushing for only two of four known blocks. Print a report listing all four with their counts — first without any pre-initialisation, then with $h{$_} //= [] for @all_blocks;. Describe the difference in one sentence.
Exercise 6 — inner-list operations. Build §4's slack structure. For each key print the count, the worst value, the number of negatives using grep in scalar context, and the mean. Then total the path count across all keys without sorting.
Exercise 7 — sorting by a property. Order the keys by list length descending with an alphabetical tiebreaker, and separately by worst inner value ascending. Print both orders. Then remove the tiebreaker and say what could change.
Exercise 8 — copy or not. Copy one key's list into my @v, push onto @v, and print both @v's count and the structure's count for that key. Then push through @{ $h{$k} } and print both again.
9. Summary
push @{ $h{$key} }, $valueis the whole pattern — it creates the inner array on first use and appends after, with no declaration and no existence check.- The outer container has no order; the inner ones keep theirs. Sort the keys when printing.
@{ $h{$k} }is the list,scalar(@{ $h{$k} })its count,$h{$k}[0]one item — no arrow needed between two subscripts.if ($h{$k})is true whenever the key exists, even for an empty list. Useif (@{ $h{$k} })to ask about contents.- Only keys you push to get created. Use
$h{$_} //= [] for @all;when every key must appear. - Inner lists are ordinary arrays —
sort,grep,mapand$#{ }all apply, andgrepin scalar context gives a count. - A comparator may use
@{ $h{$b} } <=> @{ $h{$a} }to sort keys by list length; add a tiebreaker because the sort is not guaranteed stable. - Copying a list out with
my @v = @{ ... }gives a copy. Work through the reference to modify.
Chapter 7.8 replaces the inner arrays with hashes, which is how records keyed by name are stored — and introduces a trap where merely asking whether something exists creates it.
Where this fits
Part of the Perl curriculum.
