Perl Part I · Module 7 · References
Hashes of Hashes
A hash of hashes stores records by name — the block, then the field — and it is the shape a parsed tool report naturally takes, because tool output is usually a sequence of statements about named things. Building one is as economical as the previous chapter: a single assignment to $report{$block}{$field} creates both levels with no setup. What makes this chapter Advanced is not the notation but a consequence of that economy. The same mechanism that spares you the setup also means that reaching into the second level of a key that does not exist creates the first level as a side effect — and that happens even when all you did was ask whether something was there. A question that changes the data is worth understanding properly.
Advanced16 min readPerlPart IReferencesData Structures
Part I · Module 7 · Chapter 7.8 · Hashes of Hashes
1. Records Keyed by Name
use strict;
use warnings;
my %report = (
alu => { slack => -0.12, paths => 1840, corner => "ss" },
fifo => { slack => 0.34, paths => 920, corner => "ff" },
);
for my $blk (sort keys %report) {
printf "%-5s slack %+6.2f paths %5d corner %s\n",
$blk,
$report{$blk}{slack},
$report{$blk}{paths},
$report{$blk}{corner};
}
printf "blocks : %d\n", scalar(keys %report);
printf "fields in alu : %d\n", scalar(keys %{ $report{alu} });alu slack -0.12 paths 1840 corner ss
fifo slack +0.34 paths 920 corner ff
blocks : 2
fields in alu : 3$report{$blk}{slack} is two lookups: the block, then the field. No arrow is needed between them, per Chapter 7.4 §3.
The two counts answer different questions and both get asked constantly:
| Question | Expression |
|---|---|
| how many blocks | scalar(keys %report) |
| how many fields one block has | scalar(keys %{ $report{alu} }) |
| one field | $report{alu}{slack} |
| one block's whole record | %{ $report{alu} } |
| every block name | keys %report |
| every field name of one block | keys %{ $report{alu} } |
The shape, drawn
A hash of hashes is one outer hash keyed by name, holding a record per key
Compared with Chapter 7.6's table, the gain is that fields have names. $report{alu}{slack} says what it means; $table->[0][1] does not. The loss is ordering — neither level remembers the order anything arrived in, which §3 deals with.
2. Building One From Input
A single assignment creates both levels.
use strict;
use warnings;
my @lines = (
"alu slack -0.12",
"alu paths 1840",
"fifo slack 0.34",
"fifo paths 920",
"alu corner ss",
);
my %report;
for my $line (@lines) {
my ($blk, $field, $value) = split ' ', $line;
$report{$blk}{$field} = $value;
}
for my $blk (sort keys %report) {
printf "%-5s : %s\n", $blk,
join(" ", map { "$_=$report{$blk}{$_}" } sort keys %{ $report{$blk} });
}
printf "blocks %d\n", scalar(keys %report);alu : corner=ss paths=1840 slack=-0.12
fifo : paths=920 slack=0.34
blocks 2$report{$blk}{$field} = $value; is the whole loop body. The first time a block is seen, the inner hash does not exist, and Perl creates it — the same autovivification that powered Chapter 7.7's push, and Chapter 7.9's subject.
Two things in the output are worth reading carefully:
- The fields come out alphabetical, not in input order.
alushowscorner, paths, slackalthoughcornerarrived last. The inner container is a hash;sort keysimposed the only order there is. - Blocks need not have the same fields.
fifohas two,aluhas three. Nothing pads, and nothing complains. That flexibility is useful for sparse tool output and dangerous when you assume a field is present — which is what §4 is about.
3. Walking Two Levels
use strict;
use warnings;
my %report = (
alu => { slack => -0.12, paths => 1840 },
fifo => { slack => 0.34, paths => 920 },
);
for my $blk (sort keys %report) {
for my $field (sort keys %{ $report{$blk} }) {
printf "%-5s %-6s %s\n", $blk, $field, $report{$blk}{$field};
}
}
my @failing = grep { $report{$_}{slack} < 0 } sort keys %report;
printf "failing : %s\n", join(" ", @failing);
my $total = 0;
$total += $report{$_}{paths} for keys %report;
printf "total paths : %d\n", $total;alu paths 1840
alu slack -0.12
fifo paths 920
fifo slack 0.34
failing : alu
total paths : 2760Both loops need sort, and the inner one needs sort keys %{ $report{$blk} } — the braces open the record so keys has a hash to work on.
Note the two different styles in the last few lines, and that the difference is deliberate:
grep { ... } sort keys %reportsorts because the result is printed, so it must be reproducible.$total += ... for keys %reportdoes not sort, because addition does not care about order and sorting would be wasted work.
Sort when the order reaches a human or a diff. Do not sort when you are only accumulating.
4. Asking Can Create
This is the section that makes the chapter Advanced.
use strict;
use warnings;
my %r = ( alu => { slack => -0.12 } );
printf "alu exists : %s\n", (exists $r{alu} ? "yes" : "no");
printf "alu.slack exists : %s\n", (exists $r{alu}{slack} ? "yes" : "no");
printf "alu.corner exists : %s\n", (exists $r{alu}{corner} ? "yes" : "no");
printf "fifo exists : %s\n", (exists $r{fifo} ? "yes" : "no");
printf "fifo.slack exists : %s\n", (exists $r{fifo}{slack} ? "yes" : "no");
printf "AND NOW fifo exists: %s\n", (exists $r{fifo} ? "yes" : "no");
printf "top-level keys : %d\n", scalar(keys %r);alu exists : yes
alu.slack exists : yes
alu.corner exists : no
fifo exists : no
fifo.slack exists : no
AND NOW fifo exists: yes
top-level keys : 2Read lines four, five and six in order.
exists $r{fifo}— no. Correct; there is nofifo.exists $r{fifo}{slack}— no. Also correct as an answer.exists $r{fifo}— yes.
Nothing was assigned between them. The question in step 2 created $r{fifo}, and the top-level key count is now 2 rather than 1.
The mechanism is the one that has been helping all along. To evaluate $r{fifo}{slack}, Perl must first get something hash-like out of $r{fifo} to apply {slack} to. $r{fifo} sits in a position that is allowed to be assigned, so Perl puts an empty hash reference there. The exists then correctly reports that slack is not in that brand-new empty hash — and the hash stays.
5. Deleting at Either Level
use strict;
use warnings;
my %r = (
alu => { slack => -0.12, paths => 1840 },
fifo => { slack => 0.34 },
);
delete $r{alu}{paths};
printf "alu fields after inner delete : %d [%s]\n",
scalar(keys %{ $r{alu} }), join(" ", sort keys %{ $r{alu} });
delete $r{fifo};
printf "top-level keys after delete : %d [%s]\n",
scalar(keys %r), join(" ", sort keys %r);alu fields after inner delete : 1 [slack]
top-level keys after delete : 1 [alu]delete $r{alu}{paths} removes one field; delete $r{fifo} removes a whole record. Both work as Chapter 6.3 describes, at whichever level you aim them.
Deleting the top-level key discards the inner hash as well — there is no other reference to it, so it goes. That is Chapter 7.2 §3's reference counting in reverse: the data lived while something referred to it, and now nothing does.
6. Missing Fields
Because records need not have the same fields, reading a field that is not there is a normal risk.
use strict;
use warnings;
my %r = (
alu => { slack => -0.12, corner => "ss" },
fifo => { slack => 0.34 },
);
for my $blk (sort keys %r) {
my $corner = $r{$blk}{corner} // "(none)";
printf "%-5s corner %s\n", $blk, $corner;
}alu corner ss
fifo corner (none)// supplies a default when the field is absent or undef, and it keeps STDERR clean. Drop it and the same program warns:
alu corner ss
fifo cornerUse of uninitialized value in printf at hoh-missing-nodefault.pl line 10.A blank field and a warning, exit 0 — the same pattern Chapter 7.6 §4 measured for a missing array cell.
// is the right operator here rather than || because a field whose legitimate value is 0 must survive. Measured on a record whose slack genuinely is zero:
raw value : 0
with // : 0
with || : (none)|| replaced a real measurement with (none), because 0 is false. // asks only whether the value is defined, which is the question you meant. Chapter 6.4 made this argument for flat hashes and it holds unchanged at two levels — and a slack of exactly zero is a perfectly ordinary timing result, not a hypothetical.
7. Why This Matters in Engineering Work
Tool reports are statements about named things. "This block, this field, this value" is the natural line format of a timing summary, a DRC report, a utilisation dump — and a hash of hashes is that format held in memory. The three-way split plus one assignment in §2 is the core of a great many working scripts.
Named fields survive edits that positional ones do not. Adding a column to a tool's output breaks every subscript in a positional parser and breaks nothing in this one. For anything you will still be running next year, that alone justifies the shape.
And the exists trap costs real debugging time. A script that checks for optional fields across a set of blocks can silently inflate its own block count, and the symptom — a total that is too high, or a report listing blocks that were never in the input — points nowhere near the exists. Knowing that asking can create is the fastest route to that bug.
8. Common Mistakes
Testing a deep key without guarding. exists $h{a}{b} creates $h{a}. Guard with && from the outside in.
Trusting a top-level key count after optional-field checks. The checks may have added keys.
Forgetting %{ } around a record for keys. keys %{ $r{alu} }, not keys $r{alu}.
Expecting input order from either level. Both are hashes. Sort for output.
Assuming every record has every field. They need not. Use // for a default.
Using || for the default. It replaces a legitimate 0. Use //.
Sorting inside an accumulation loop. Wasted work; sort only where order is observed.
9. Exercise
Exercise 1 — records. Build §1's two-block structure with three fields each. Print the formatted table, the block count, and the field count for one block. Note which of the last two needed %{ }.
Exercise 2 — build from triples. Take §2's five block field value lines, split each into three, and build the structure with one assignment. Print each block's fields joined, sorted. Confirm the output order differs from the input order and say why.
Exercise 3 — uneven records. In Exercise 2's data, confirm one block has three fields and the other two. Then print the field count per block and state what would have to change for Perl to object.
Exercise 4 — the two-level walk. Print every block/field/value triple with both levels sorted. Then list the failing blocks with grep, and total one numeric field without sorting. Say why one of those sorted and the other did not.
Exercise 5 — the trap. Reproduce §4 exactly, including the repeated exists $r{fifo} before and after. Record both answers and the final top-level key count. Write one sentence naming the line that created the key.
Exercise 6 — the guard. Starting from a one-key hash, print the key count, then run an unguarded exists $r{fifo}{slack} and print it again, then reset and run exists $r{regf} && exists $r{regf}{slack} and print it again. Report all three counts.
Exercise 7 — delete. Build a two-block structure where one block has two fields. Delete one inner field and print that block's field count, then delete a whole block and print the top-level count.
Exercise 8 — missing fields. Build two records where only one has a corner field. Print both with // supplying (none), and confirm STDERR is empty. Then remove the // and record what reaches STDERR. Finally, set a field to 0 and show that || and // disagree about it.
10. Summary
$report{$block}{$field}is two named lookups — no arrow needed between them.- One assignment builds both levels.
$report{$blk}{$field} = $value;needs no setup. scalar(keys %report)counts blocks;scalar(keys %{ $report{$blk} })counts one block's fields.- Neither level has an order. Sort both when printing; do not sort when accumulating.
- Records need not share fields, and nothing warns when they differ.
- Reaching into level two of a missing level-one key creates it — including from
exists. Measured: one top-level key before, two after. - Guard with
&&from the outside in, and the short-circuit prevents the creation. Measured: the key count stays at one. deleteworks at either level, and deleting a record discards its inner hash.- Use
//for a missing field's default, never||, so a legitimate0survives.
Chapter 7.9 takes autovivification head on: exactly which positions trigger it, why it is a feature rather than a bug, and how to switch it off where you need a lookup that cannot change anything.
Where this fits
Part of the Perl curriculum.
