Perl Part I · Module 5 · Arrays and Lists
sort and Custom Comparators
Sorting is where a report stops being a dump and starts being useful, because the row a reader needs is at the top. Perl's sort does the work; what you supply is a small block saying how to compare two items, and that block is where all the decisions live — compare as numbers or as text, ascending or descending, on the whole value or on one field pulled out of it. The block has one peculiarity worth knowing before you write your first one. The two values being compared arrive in variables that are deliberately not lexical, so the habit this track has taught you since Chapter 1.10 — declare everything with my — is the one thing that breaks it. Perl warns, and then hands you a list that is sorted wrongly.
Foundation14 min readPerlPart IFundamentalsArraysSortingReports
Part I · Module 5 · Chapter 5.9 · sort and Custom Comparators
1. The Default Is a String Sort
use strict;
use warnings;
my @counts = (10, 9, 100, 2);
print "default : ", join(" ", sort @counts), "\n";
print "numeric : ", join(" ", sort { $a <=> $b } @counts), "\n";default : 10 100 2 9
numeric : 2 9 10 100sort @list with no block compares with cmp, which is a text comparison — Chapter 3.6 §3 and Chapter 4.11 §2 both measured this. 10 100 2 9 is correct character ordering and wrong for a column of counts.
The fix is a block supplying <=> instead, the three-way numeric comparator from Chapter 3.7.
2. How the Block Works
sort { COMPARISON } @listPerl repeatedly hands the block two elements in the variables $a and $b, and the block must return:
- a negative number if
$ashould come first, - zero if their order does not matter,
- a positive number if
$bshould come first.
Which is exactly what <=> and cmp return — that is why they exist, and why Chapter 3.7 called the three-way answer "what sorting needs".
| Block | Orders by |
|---|---|
{ $a <=> $b } | number, ascending |
{ $b <=> $a } | number, descending |
{ $a cmp $b } | text, ascending — same as no block |
{ $b cmp $a } | text, descending |
And sort returns a new list — Chapter 4.11 §2 measured that the original array is untouched. Assign it back if you want the array reordered.
3. my $a Breaks sort
This is the chapter's sharp edge, and it is sharp because the result is wrong rather than fatal.
use strict;
use warnings;
my @n = (3, 1, 2);
my $a = 99;
my @sorted = sort { $a <=> $b } @n;
print "result: @sorted\n";result: 2 1 3"my $a" used in sort comparison at broken-sort.pl line 8.2 1 3. Not sorted, and not obviously unsorted either — it is a permutation of the input, which is what a sort is supposed to produce.
Remove the my $a = 99; line and the same script gives 1 2 3.
4. Sorting on a Computed Field
The block can compare anything derived from $a and $b, not just the values themselves.
use strict;
use warnings;
my @rows = ("alu 184", "fifo 92", "decoder 1240", "regfile 300");
print "by name : ", join(" | ", sort @rows), "\n";
print "by count : ", join(" | ", sort { (split / /, $a)[1] <=> (split / /, $b)[1] } @rows), "\n";
print "desc : ", join(" | ", sort { (split / /, $b)[1] <=> (split / /, $a)[1] } @rows), "\n";by name : alu 184 | decoder 1240 | fifo 92 | regfile 300
by count : fifo 92 | alu 184 | regfile 300 | decoder 1240
desc : decoder 1240 | regfile 300 | alu 184 | fifo 92Each comparator splits both values and compares their second field — using the list slice from Chapter 5.6 §5 to take one field without an intermediate array.
Note the first row. sort @rows ordered by name, because the whole string was compared as text and the name comes first in it. That is the Chapter 3.6 trap in a realistic shape: the output looks deliberately ordered, and a reader expecting "sorted by count" has no reason to doubt it.
5. Tie-Breaking, and What Perl Does Not Promise
use strict;
use warnings;
my @rows = ("b 1", "a 1", "c 1", "d 0");
print "one key : ", join(" | ", sort { (split / /, $a)[1] <=> (split / /, $b)[1] } @rows), "\n";
print "with tie: ", join(" | ", sort {
(split / /, $a)[1] <=> (split / /, $b)[1]
or
$a cmp $b
} @rows), "\n";one key : d 0 | b 1 | a 1 | c 1
with tie: d 0 | a 1 | b 1 | c 1Three rows tie on the numeric key. In the first line they came out b a c — their input order, in this run. In the second they came out a b c, ordered by the tie-break.
6. A Worked Report
Putting the module together — accumulate, guard, sort, format:
use strict;
use warnings;
my @raw = (
"alu,184,177",
"fifo,92,92",
"decoder,1240,1198",
"regfile,300,271",
);
my @rows;
for my $line (@raw) {
my @field = split /,/, $line, -1;
next if scalar(@field) != 3;
my ($name, $total, $passed) = @field;
push @rows, sprintf("%-10s %6d %6d %6.1f", $name, $total, $passed, $passed / $total * 100);
}
printf "%-10s %6s %6s %6s\n", "block", "total", "passed", "rate";
for my $row (sort { (split " ", $a)[3] <=> (split " ", $b)[3] } @rows) {
print "$row\n";
}block total passed rate
regfile 300 271 90.3
alu 184 177 96.2
decoder 1240 1198 96.6
fifo 92 92 100.0Worst rate first, which is the order someone triaging a regression wants.
Four module chapters are doing the work: the guard from Chapter 5.8, the accumulator from Chapter 5.4 §5, sprintf from Chapter 4.8, and the comparator here. Note the comparator uses split " " — Chapter 4.7 §5's whitespace form — because the rows are now sprintf-padded and a literal single space would produce empty fields.
Building the formatted lines before sorting is what makes this work: the rate is already in the string, so the comparator can read it back out. The alternative — sorting the raw data and formatting afterwards — is cleaner but needs a way to keep the computed rate attached to the row, which again is Module 7's.
7. Why This Matters in Engineering Work
The top of the report is where people look. Worst slack first, most failures first, lowest coverage first. The comparator decides what gets attention, so it is a correctness question and not a presentation one.
The default sort is wrong for every numeric column, and wrong in a way that looks right — Chapter 3.6 §3's 1 10 100 11 2. Any sort of counts, cycles, slacks or percentages needs <=>.
my $a is a genuine landmine in a long script. A variable named $a three hundred lines above a sort silently breaks it, and the warning scrolls past in a run that produces output. Never use those two names.
And reproducibility is worth the four characters. A total comparator means today's report and yesterday's differ only where the data differs, which is the whole point of diffing them.
8. Common Mistakes
Omitting the block for numbers. sort @counts is a text sort: 10 before 9.
Declaring my $a or my $b anywhere in scope. Shadows what sort sets; the sort returns a wrongly ordered list and warns once.
Swapping the wrong pair for descending. { $b <=> $a } is descending; { $a <=> $b } reversed afterwards with reverse works too, but writing both is a double negative.
Assuming equal keys keep their input order. Not guaranteed. Add a tie-break or use sort 'stable'.
Expecting sort to reorder the array. It returns a new list.
Comparing with <=> when the key is text, or cmp when it is a number. Chapter 3.7 §2 measured that these give opposite signs on the same pair.
Splitting inside a comparator on a huge list without measuring. Fine for hundreds of rows; worth revisiting if a sort becomes noticeably slow.
9. Exercise
Exercise 1 — the default trap. Sort (7, 70, 7000, 8) with no block and with { $a <=> $b }. Record both orders and say which you would put in a column headed "cycles".
Exercise 2 — break it with my $a. Reproduce §3: sort three numbers with a my $a = 99; in scope. Record the result and the warning. Then remove the declaration and confirm the order changes. State in one sentence why use strict did not prevent this.
Exercise 3 — four directions. Given ("10", "9", "100"), produce all four orderings from §2's table and label each.
Exercise 4 — a computed key. Given rows of the form "name count", sort them by count descending. Then sort by the length of the name, ascending.
Exercise 5 — make it reproducible. Sort ("b 1", "a 1", "c 1") by the numeric field alone, then add a tie-break on the name. Explain in one sentence what the tie-break guarantees that the first version does not.
Exercise 6 — worst first. Build four rows of name,total,passed, compute the pass rate, and print a report ordered worst rate first with aligned columns. Confirm the lowest rate is on the first row.
Exercise 7 — the wrong operator. Sort ("10", "9", "100") once with { $a <=> $b } and once with { $a cmp $b }. Record both and name the chapter that explains why the two disagree.
10. Summary
sortwith no block is a text sort, so10comes before9. Numeric columns need{ $a <=> $b }.- The block receives two values in
$aand$band returns negative, zero or positive — which is what<=>andcmpproduce. $aand$bare package variables thatuse strictexempts. A lexicalmy $ashadows them, the comparator compares a constant, and you get a wrongly ordered list with one warning. Never name a variable$aor$b.- A comparator can compare anything derived from the values, including a field pulled out with a list slice.
- Stability is not guaranteed by default. Add a tie-break —
or $a cmp $b— oruse sort 'stable';. sortreturns a new list and leaves the array alone.- A comparator that splits runs on every comparison. Readable first; optimise only when measured.
Chapter 5.10 covers the two functions that replace most loops over a list — one that keeps the elements matching a condition and one that transforms every element — including the fact that the loop variable inside them is an alias to the original.
Where this fits
Part of the Perl curriculum.
