Skip to content
VLSI Mentor

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

Azvya Education Pvt. Ltd.VLSI Mentor
hoa.pl
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} });
}
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output
alu   4 port(s) : clk rst a b
fifo  2 port(s) : clk wr_en

Look 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:

QuestionExpression
the list for one key@{ $ports{alu} }
how many items that key hasscalar(@{ $ports{alu} })
one item$ports{alu}[0]
its last index$#{ $ports{alu} }
every keykeys %ports
append to one key's listpush @{ $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.

Azvya Education Pvt. Ltd.VLSI Mentor
hoa-group.pl
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);
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output
alu   (3) : clk, rst, a
fifo  (2) : clk, wr_en
blocks seen : 2

The 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 split and one push. There is no bookkeeping.
  • sort keys %by_block gives deterministic output. Without the sort the block order would vary between runs, as Chapter 6.5 established and Chapter 6.6 addressed. Any report you intend to diff needs the sort.
  • Within a key, order is preserved. alu reads clk, 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.

Azvya Education Pvt. Ltd.VLSI Mentor
hoa-empty.pl
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;
}
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output
alu   ref is true, list is true, count 1
fifo  ref is true, list is false, count 0

fifo 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:

Azvya Education Pvt. Ltd.VLSI Mentor
hoa-bugfix.pl — both reports, same data
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"; }
}
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output
BUGGY  if ($fail{$blk}):
  alu FAILED
  fifo FAILED
FIXED  if (@{ $fail{$blk} }):
  alu FAILED

if ($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.

Azvya Education Pvt. Ltd.VLSI Mentor
hoa-ops.pl
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;
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output
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 : 6

Two idioms in there are worth pointing out:

  • my $neg = grep { ... } @v; — grep in scalar context returns the count of matches, not the matches. That is Chapter 5.10 §5's behaviour and it saves a scalar(my @tmp = grep ...).
  • $total += scalar(@{ $slack{$_} }) for keys %slack; — summing across the inner lists without caring about key order, so no sort is needed. Use sort when 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.

Azvya Education Pvt. Ltd.VLSI Mentor
hoa-sort.pl
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};
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output
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} }, $value is 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. Use if (@{ $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, map and $#{ } all apply, and grep in 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.