Perl Part I · Module 6 · Hashes
Choosing Between an Array and a Hash
Two modules have given you two containers, and this chapter closes the pair by deciding between them. The usual advice is that order means an array and lookup by name means a hash, which is right as far as it goes — but it is more useful seen from the other side, as what the wrong choice costs you. Pick an array when you need lookup and every lookup becomes a nested loop. Pick a hash when you need order and you do not merely get the items in a different order, you can get a different set of items on each run. There is also a third question the usual advice leaves out, which settles cases the first two do not: whether repeated values are data or noise.
Foundation15 min readPerlPart IFundamentalsHashesArraysData Structures
Part I · Module 6 · Chapter 6.11 · Choosing Between an Array and a Hash
1. Three Questions, Not Two
| Question | Array | Hash |
|---|---|---|
| Does position mean something? | yes — it is the whole point | no — there is no position |
| Do you look things up by name? | only by scanning | yes — directly |
| Do repeats matter? | kept, every one | collapsed to one key |
The first two are the usual advice. The third decides the cases the first two leave open, and §5 is about it.
Read the table as what each container is for: an array is a sequence, a hash is a mapping. Everything else follows.
2. The Decision, Drawn
One question decides it, and the answer brings its own operations
3. An Array Where You Needed a Hash
use strict;
use warnings;
my @rows = ("alu PASS", "fifo FAIL", "decoder PASS", "regfile FAIL");
print "with an array — a scan per lookup:\n";
for my $wanted (qw(fifo regfile)) {
my $found;
for my $row (@rows) {
my ($name, $status) = split " ", $row;
if ($name eq $wanted) { $found = $status; last }
}
printf " %-8s %s\n", $wanted, (defined $found ? $found : "not found");
}
my %status = map { split " ", $_ } @rows;
print "with a hash — one lookup each:\n";
for my $wanted (qw(fifo regfile)) {
printf " %-8s %s\n", $wanted, ($status{$wanted} // "not found");
}with an array — a scan per lookup:
fifo FAIL
regfile FAIL
with a hash — one lookup each:
fifo FAIL
regfile FAILIdentical answers. The array version is not wrong — it is what the structure forces you to write.
Count what the array version needs that the hash version does not:
- a nested loop,
- a
splitof every row on every lookup, - a
$foundvariable initialised outside the inner loop, - a
lastso it stops at the first match, - and a
definedcheck because$foundmay never be set.
Five things, each a place to get it wrong. The hash version is $status{$wanted}.
And the hash was built in one line — map { split " ", $_ } @rows — using Chapter 5.10's map returning two items per row, which flatten into pairs by Chapter 5.12's rule and become a hash by Chapter 6.2 §1's. Build the hash once, then look up as often as you like. That is Chapter 5.11 §1's point, now with the alternative written out so the cost is visible.
4. A Hash Where You Needed an Array
The reverse is worse, because it does not just make the code awkward — it makes the answer wrong.
use strict;
use warnings;
my @lines = ("start", "loading", "running", "done");
printf "array, first three : %s\n", join(" | ", @lines[0 .. 2]);
my %by_line;
$by_line{$_} = 1 for @lines;
printf "hash, first three : %s\n", join(" | ", (keys %by_line)[0 .. 2]);Three runs:
hash, first three : start | running | loading
hash, first three : done | running | start
hash, first three : running | done | loadingarray, first three : start | loading | runningLook carefully at the three hash lines. They are not three orderings of the same three items:
- Run 1 has
start,running,loading. - Run 2 has
done,running,start—doneis in andloadingis out. - Run 3 has
running,done,loading—startis out.
Different sets, not just different orders. "The first three keys of a hash" is not a reordering of the right answer; it is an arbitrary three of four.
Your three runs will show different lines again — repeating this capture produced done | start | running, running | loading | start and loading | done | start, which between them exclude a different item each time. The reproducible fact is that the subset is arbitrary, not which subset you get.
5. The Third Question: Do Repeats Matter?
use strict;
use warnings;
my @runs = ("alu", "fifo", "alu");
my %seen;
$seen{$_} = 1 for @runs;
printf "array : %d entries\n", scalar(@runs);
printf "hash : %d keys\n", scalar(keys %seen);array : 3 entries
hash : 2 keysThree in, two out. The second alu is gone — not an error, just the overwrite from Chapter 6.3 §3.
That difference decides cases the first two questions do not:
| What you are storing | Container | Why |
|---|---|---|
| every test run, including reruns | array | each run is a separate fact |
| the final status per test | hash | one answer per name |
| how many times each test ran | hash of counts | Chapter 6.8 |
| which tests ran at all | hash as a set | Chapter 6.9 |
Note that the last two are hashes because collapsing duplicates is what you want — you have turned the repeats into a number or ignored them deliberately. That is the distinction: a hash is right when repeats are noise or when counting them is the job, and wrong when each occurrence is its own data point.
6. When You Need Both
The honest answer is often that you need order and lookup, and the answer is to keep both.
use strict;
use warnings;
my @order;
my %status;
for my $row ("alu PASS", "fifo FAIL", "decoder PASS") {
my ($name, $st) = split " ", $row;
push @order, $name;
$status{$name} = $st;
}
printf "in file order : %s\n", join(" | ", @order);
printf "lookup fifo : %s\n", $status{fifo};
print "report:\n";
printf " %-8s %s\n", $_, $status{$_} for @order;in file order : alu | fifo | decoder
lookup fifo : FAIL
report:
alu PASS
fifo FAIL
decoder PASSTwo lines in the loop and you have both properties. The array remembers the sequence; the hash answers the questions. The report iterates the array and looks up the hash, so it comes out in file order with direct access to each value.
This is a genuinely common and correct pattern, and it is worth recognising as the answer rather than as a workaround. It costs one extra push.
Two details:
- The array holds keys, not values.
@orderis a list of names; the data lives once, in the hash. Duplicating the values in both would give you two things to keep in step. - If the input can repeat a name,
push @order, $name unless exists $status{$name};keeps the first-seen order and avoids a duplicate row in the report — Chapter 6.9's set guard doing the job.
7. A Decision Checklist
In the order the questions actually settle things:
- Is each item a separate occurrence, even if it repeats? → array.
- Does the order they arrived in matter? → array, or array-plus-hash if you also need lookup.
- Do you look items up by a name? → hash.
- Do you need to know how many times each thing appeared? → hash of counts.
- Do you only need to know whether you have seen something? → hash as a set.
- None of the above — just a bag of values you will process in bulk? → array; it is simpler and it keeps everything.
And the signal that you chose wrong, in either direction:
- Writing a loop to find something means you wanted a hash.
- Sorting the keys to recover an order you had on input means you wanted an array — the sort will give you an order, not the order.
8. Why This Matters in Engineering Work
Both containers appear in nearly every real script, usually in exactly §6's pairing: a list of tests in submission order, and a hash of results keyed by test name. Recognising that as the normal shape saves inventing something worse.
The lookup mistake costs performance, visibly. A nested scan over a few hundred rows is imperceptible; over a hundred thousand lines of a timing report with a lookup per line, it is the difference between a script that finishes and one you kill. And the fix is one line that builds a hash.
The order mistake costs correctness, invisibly. §4's measurement is the one to remember: a hash does not give you the items in a shuffled order, it can give you a different subset when you take the first few. A script that reports "the first three failures" from a hash reports three arbitrary failures, and the output looks entirely plausible.
And the duplicates question is the one people skip. A hash keyed on test name silently keeps one result per test, which is right for a final-status report and wrong for a log of every attempt. Deciding that deliberately — rather than discovering it when a rerun's result overwrites the original — is what §5 is for.
9. Common Mistakes
Scanning an array in a loop to find something by name. Build a hash once instead.
Storing a sequence in a hash. The order is not recoverable by sorting, because it was never stored.
Taking (keys %h)[0 .. 2] as "the first three". It is an arbitrary three, and a different three on the next run.
Using a hash for data where each occurrence matters. Repeats collapse and the earlier value is lost.
Duplicating values into both an array and a hash. Keep the keys in the array and the data in the hash, once.
Reaching for a complicated structure when an array would do. If you process everything in bulk and never look anything up, an array is simpler and loses nothing.
Assuming sorting fixes every order problem. It gives a defined order, not the input's order.
10. Exercise
Exercise 1 — the three questions. For each of these, say which container and which of §1's three questions decided it: (a) the lines of a log file; (b) the final status of each test; (c) how many warnings of each type; (d) every warning message emitted, including repeats; (e) the set of blocks that were touched; (f) the order tests were submitted in.
Exercise 2 — count the cost. Reproduce §3's script. Then list every construct the array version needs that the hash version does not, and say how many split calls each version performs in total.
Exercise 3 — the wrong set. Run §4's script three times and record the hash line each time. Report whether the three lines contain the same three items, then say in one sentence why sorting would not fix it.
Exercise 4 — order is gone. Store four log lines in a hash, then try to recover the original order. Say what you would need to have stored to make it possible.
Exercise 5 — duplicates. Store a three-item list containing one repeat in both an array and a hash. Report both counts, then say which you would use to answer "how many runs were there" and which for "what was the final result".
Exercise 6 — both. Reproduce §6's pattern over four rows, then add a duplicate name to the input and confirm the report shows it once, in first-seen position, using the unless exists guard.
Exercise 7 — diagnose a choice. You are shown a script containing a nested loop that searches an array by name, and another that sorts hash keys to print "the first five events". Say what each author should have used and what symptom each mistake produces.
11. Summary
- Three questions decide it: does position mean something, do you look up by name, and do repeats matter.
- An array is a sequence; a hash is a mapping. Everything else follows from that.
- Using an array for lookup forces a nested loop, a
splitper row per lookup, and four other moving parts — all replaced by one subscript. - Using a hash for order loses the order permanently. Taking the first three keys gave a different set of three on each run, not a different order — and no sort recovers the input's sequence.
- A hash collapses duplicates. That is right when repeats are noise or when counting them is the job, and wrong when each occurrence is a separate fact.
- Needing both is normal:
pushthe key onto an array for order and assign into a hash for lookup, then iterate the array and look up the hash. - Writing a loop to find something means you wanted a hash; sorting keys to recover an input order means you wanted an array.
That closes Module 6, and with it the two containers. You can hold values in order and find them by position, hold them by name and find them directly, tell the three questions about a key apart, walk a hash reproducibly, count and deduplicate in one line each, pass named arguments, and choose the right container for a job.
Module 7 lifts the restriction both modules kept running into. Chapter 5.12 could not return two arrays from a subroutine; Chapter 6.3 could not give one key several values; Chapter 5.8 had to store parsed rows with a construct it could only call a forward pointer; and Chapter 6.10 could not pass two hashes separately. All four need a scalar that stands for a whole array or hash, and that is a reference — which is what makes a table of rows, a list of failures per block, and a record with named fields possible.
Where this fits
Part of the Perl curriculum.
