Perl Part I · Module 6 · Hashes
Counting With a Hash
This is the idiom Perl is known for and it is one line long. Walk a list, increment a hash entry keyed on each item, and at the end the hash holds how many times each distinct item appeared — with no initialisation, no check for whether the key already exists, and no warning about the absent value you just incremented. That last part is worth understanding rather than just using, because Perl is doing two separate favours: creating the entry the moment you increment it, and exempting the increment from the warning it would normally give for using an undefined value. Both have names, both have limits, and one of them does not extend to the operator you might reach for next.
Foundation14 min readPerlPart IFundamentalsHashesIdiomsReports
Part I · Module 6 · Chapter 6.8 · Counting With a Hash
1. The Whole Idiom
use strict;
use warnings;
my @statuses = qw(PASS FAIL PASS PASS TIMEOUT FAIL PASS);
my %count;
$count{$_}++ for @statuses;
for my $status (sort { $count{$b} <=> $count{$a} or $a cmp $b } keys %count) {
printf "%-8s %d\n", $status, $count{$status};
}
printf "distinct: %d total: %d\n", scalar(keys %count), scalar(@statuses);PASS 4
FAIL 2
TIMEOUT 1
distinct: 3 total: 7The counting is one line:
$count{$_}++ for @statuses;No my %count = (); with keys pre-created. No if (exists $count{$_}). No initialisation to zero. Each ++ either increments an existing entry or creates it at 1.
The rest of the script is reporting, and both halves are earlier chapters doing their jobs:
sort { $count{$b} <=> $count{$a} or $a cmp $b }orders by count descending with a tie-break, from Chapter 5.9 §5 — and the tie-break is what makes the output reproducible, which Chapter 6.6 §4 showed matters for a hash.scalar(keys %count)is the number of distinct values andscalar(@statuses)the number of observations. Two different numbers that a report usually wants both of.
The for as a statement modifier is Chapter 3.9 §1's form, and $_ is Chapter 2.12 §4's default variable.
2. Why It Needs No Initialisation
use strict;
use warnings;
my %count;
printf "before : %d key(s)\n", scalar(keys %count);
$count{alu}++;
printf "after : %d key(s), alu = %d\n", scalar(keys %count), $count{alu};before : 0 key(s)
after : 1 key(s), alu = 1An empty hash, one ++, and now there is a key with the value 1. Nothing on STDERR.
Two things happened, and they are separate:
- Perl created the entry. The key
aludid not exist and now it does. This is called autovivification — Perl bringing a container entry into existence because you used it in a way that implies it should be there. Chapter 7.9 covers it properly, including the nested case where it does considerably more work. - The increment did not warn.
$count{alu}was undefined, and++on an undefined value gives 1 without complaint.
That second point is the one worth checking rather than assuming, because Chapter 1.11 has been insisting all along that using an undefined value produces a diagnostic.
3. The Exemption Follows the Operator, Not the Container
use strict;
use warnings;
my %a;
$a{k}++;
printf "++ : value=%d keys=%d\n", $a{k}, scalar(keys %a);
my %b;
$b{k} += 1;
printf "+= : value=%d keys=%d\n", $b{k}, scalar(keys %b);
my %c;
my $sum = $c{k} + 1;
printf "+ : value=%d keys=%d\n", $sum, scalar(keys %c);++ : value=1 keys=1
+= : value=1 keys=1
+ : value=1 keys=0Use of uninitialized value $c{"k"} in addition (+) at three-operators.pl line 13.Three operators, three different behaviours:
| Operator | Warns? | Creates the key? |
|---|---|---|
$h{k}++ | no | yes |
$h{k} += 1 | no | yes |
$h{k} + 1 | yes | no |
The first two are assignments to the element, so Perl creates it and exempts the operator from the undefined-value warning. The third only reads the element — the result goes somewhere else — so there is nothing to create, and reading an undefined value warns as normal.
4. Counting Things Other Than Items in a List
The idiom generalises to anything you can turn into a key.
use strict;
use warnings;
my @rows = (
"alu,PASS",
"fifo,FAIL",
"decoder,PASS",
"regfile,TIMEOUT",
"cpu,FAIL",
"dma,PASS",
);
my %by_status;
my %by_initial;
for my $row (@rows) {
my ($block, $status) = split /,/, $row, -1;
$by_status{$status}++;
$by_initial{ substr($block, 0, 1) }++;
}
printf "by status:\n";
printf " %-8s %d\n", $_, $by_status{$_} for sort keys %by_status;
printf "by first letter:\n";
printf " %-8s %d\n", $_, $by_initial{$_} for sort keys %by_initial;by status:
FAIL 2
PASS 3
TIMEOUT 1
by first letter:
a 1
c 1
d 2
f 1
r 1Two tallies from one pass. The key can be a field, a computed value, a substr — anything that produces a string.
Note both reports use sort keys, for Chapter 6.6's reason, and substr is Chapter 4.5's.
Summing rather than counting
The same shape with += adds up values instead of occurrences:
use strict;
use warnings;
my @rows = ("alu,184", "fifo,92", "alu,56", "decoder,1240", "fifo,8");
my %total;
my %runs;
for my $row (@rows) {
my ($block, $tests) = split /,/, $row, -1;
$total{$block} += $tests;
$runs{$block}++;
}
for my $block (sort keys %total) {
printf "%-10s %5d test(s) over %d run(s)\n", $block, $total{$block}, $runs{$block};
}alu 240 test(s) over 2 run(s)
decoder 1240 test(s) over 1 run(s)
fifo 100 test(s) over 2 run(s)Two hashes keyed the same way give a total and a count, and dividing one by the other would give a mean. This is the shape of most aggregation a flow script needs, and it is the honest answer to Chapter 6.3 §3's duplicate-row problem: when a key appears twice and both values matter, accumulate rather than overwrite.
5. Reading a Count That Might Not Exist
Once the tally is built, looking up a key that never appeared is Chapter 6.4's territory, and the right answer depends on the question.
use strict;
use warnings;
use v5.10;
my @statuses = qw(PASS FAIL PASS);
my %count;
$count{$_}++ for @statuses;
for my $status (qw(PASS FAIL TIMEOUT)) {
printf "%-8s seen %d time(s) (exists: %s)\n",
$status,
$count{$status} // 0,
(exists $count{$status} ? "yes" : "no");
}PASS seen 2 time(s) (exists: yes)
FAIL seen 1 time(s) (exists: yes)
TIMEOUT seen 0 time(s) (exists: no)$count{$status} // 0 reads as "the count, or zero if there is no entry" — the defined-or operator from Chapter 3.4 §4, which is correct here because a count of zero and a missing entry mean the same thing for a tally.
That is a case where // and || would both work, since a count is never legitimately empty-string and a stored 0 cannot occur in a tally built by ++. Use // anyway — it is right for the general case, and a tally built with += from data can contain a real zero.
What // does not do is tell you that TIMEOUT never appeared, which is why the exists column is there. For a report listing every expected status, "seen 0 times" is the right output. For one asking "were any statuses present that we did not expect", exists over the keys is the test.
6. Why This Matters in Engineering Work
Tallying is most of what a summary script does. Failures per block, violations per type, warnings per category, tests per corner. All of it is $count{$key}++ in a loop, and the two numbers it produces — distinct keys and total observations — are both report lines.
Distinct-count is free. scalar(keys %count) answers "how many different kinds of warning did we see", which is often the more useful number than the total. No extra work.
Accumulating is the honest answer to duplicate keys. Chapter 6.3 §3 showed a hash silently keeping the last of two rows with the same key. If both rows are real data, += into a total and ++ into a run count keeps everything, and the two hashes together say more than either alone.
And the sort is not optional. A tally printed with an unsorted keys walk changes order between runs — Chapter 6.6 — so the report cannot be diffed. Sorting by count descending with a tie-break gives both the useful order and reproducibility.
7. Common Mistakes
Initialising before incrementing. $count{$k} //= 0; then $count{$k}++ is harmless noise; ++ handles it.
Expecting ++ on an absent key to warn. It does not, and it creates the key.
Expecting a plain + to behave the same. $h{k} + 1 warns and creates nothing.
Printing a tally with unsorted keys. The order changes between runs.
Sorting by count without a tie-break. Equal counts leave the order unspecified.
Using || to default a count read from a += tally. A legitimate zero total would be replaced. Use //.
Overwriting when you meant to accumulate. $total{$k} = $v loses the earlier value; += keeps both.
8. Exercise
Exercise 1 — the idiom. Tally a list of eight status strings with one line, then print the counts ordered highest-first with a tie-break, plus the distinct and total counts.
Exercise 2 — no initialisation. Start with an empty hash, increment one key, and print the key count and value. Confirm STDERR is empty, then name the two separate favours Perl did.
Exercise 3 — three operators. Reproduce §3's script. Record which operators warned and which created a key, then fill in the three-row table from your own output.
Exercise 4 — the related exemption. Say which chapter measured the same exemption for .= on a plain scalar, and state the rule in one sentence using the word "operator".
Exercise 5 — two tallies in one pass. From six rows of block,status, build a count by status and a count by the block's first letter. Print both, sorted.
Exercise 6 — total and mean. From five rows of block,count where two blocks repeat, build a total and a run count per block, then print the mean per run to one decimal place.
Exercise 7 — the absent key. After tallying three statuses, report on four — including one that never appeared — using // 0 for the count and exists for whether it was seen. Confirm the absent one reads "0" and "no".
9. Summary
$count{$thing}++is the whole counting idiom — one line, no initialisation, no existence check.- Perl creates the entry on first use. That is autovivification, and Chapter 7.9 covers it fully.
++and+=on an absent key do not warn and do create it. Plain+warns and creates nothing.- The exemption belongs to the operator, not the hash — the same rule Chapter 3.4 §3 measured for
.=. scalar(keys %count)is the distinct count; the list's length is the total. Both are useful report lines.+=sums instead of counting, and a second hash with++gives the number of contributions — which is the right answer when a duplicate key carries real data.- Read a possibly-absent count with
// 0, and useexistswhen "never seen" is a distinct outcome. - Sort the output, with a tie-break, or the report is not reproducible.
Chapter 6.9 uses the same structure for a different purpose — ignoring the values entirely and treating the keys as a set, which is how Perl answers "have I seen this before" and removes duplicates from a list.
Where this fits
Part of the Perl curriculum.
