Skip to content
VLSI Mentor

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

Azvya Education Pvt. Ltd.VLSI Mentor
default-sort.pl
use strict;
use warnings;

my @counts = (10, 9, 100, 2);

print "default : ", join(" ", sort @counts),                  "\n";
print "numeric : ", join(" ", sort { $a <=> $b } @counts),     "\n";
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output
default : 10 100 2 9
numeric : 2 9 10 100

sort @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

Azvya Education Pvt. Ltd.VLSI Mentor
The shape
sort { COMPARISON } @list

Perl repeatedly hands the block two elements in the variables $a and $b, and the block must return:

  • a negative number if $a should come first,
  • zero if their order does not matter,
  • a positive number if $b should 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".

BlockOrders 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.

Azvya Education Pvt. Ltd.VLSI Mentor
broken-sort.pl
use strict;
use warnings;

my @n = (3, 1, 2);

my $a = 99;

my @sorted = sort { $a <=> $b } @n;

print "result: @sorted\n";
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output — STDOUT
result: 2 1 3
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output — STDERR
"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.

Azvya Education Pvt. Ltd.VLSI Mentor
by-field.pl
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";
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output
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 92

Each 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

Azvya Education Pvt. Ltd.VLSI Mentor
ties.pl
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";
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output
one key : d 0 | b 1 | a 1 | c 1
with tie: d 0 | a 1 | b 1 | c 1

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

Azvya Education Pvt. Ltd.VLSI Mentor
report.pl
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";
}
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output
block       total passed   rate
regfile       300    271   90.3
alu           184    177   96.2
decoder      1240   1198   96.6
fifo           92     92  100.0

Worst 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

  • sort with no block is a text sort, so 10 comes before 9. Numeric columns need { $a <=> $b }.
  • The block receives two values in $a and $b and returns negative, zero or positive — which is what <=> and cmp produce.
  • $a and $b are package variables that use strict exempts. A lexical my $a shadows them, the comparator compares a constant, and you get a wrongly ordered list with one warning. Never name a variable $a or $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 — or use sort 'stable';.
  • sort returns 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.