Sorting
This page walks through how Perl'ssort function actually works, from the basic $a and $b variables to the Schwartzian Transform.
The sort function hands you two magic variables called $a and $b. They hold the two items Perl is currently comparing, and your job is to return a number that tells Perl which one should come first.
That is the entire contract. The spaceship operator (sort { $a <=> $b } @numbers
<=>) returns a negative, zero, or positive number so Perl can order the list.
Part 1: THE TWO OPERATORS
Perl ships with two comparison operators for sorting:The number your block returns tellsOPERATOR TYPE RETURNS -------- -------- -------------------------------- <=> Numeric -1 if $a < $b, 0 if equal, 1 if $a > $b cmp String Same, but compares as strings
sort which element should appear first:
You do not have to return exactly negative one, zero, or one. Any negative, zero, or positive number works, but these two operators already give you the exact values-1 $a comes before $b 0 They're equal (order doesn't matter) 1 $b comes before $a
sort expects.
Part 2: NUMERIC SORTING
Perl sorts strings by default, so a list of numbers gets ordered by character code rather than magnitude:Output:my @nums = (1, 20, 3, 100, 15); print join(", ", sort @nums);
That result follows ASCII order, not numeric order. The string1, 100, 15, 20, 3
"100" lands before "15" because Perl compares the strings character by character, and "10" sorts before "15" in that comparison.
The fix is to use the spaceship operator:
my @nums = (1, 20, 3, 100, 15);
print join(", ", sort { $a <=> $b } @nums);
Output:
1, 3, 15, 20, 100
.--.
|o_o |
|:_/ |
// \ \
(| | )
/'\_ _/`\
\___)=(___/
Part 3: DESCENDING ORDER
You can flip the comparison by swapping $a and $b:
sort { $b <=> $a } @numbers # Largest first
sort { $b cmp $a } @strings # Z before A
You can also wrap the call in reverse, which is less efficient but reads clearly:
reverse sort { $a <=> $b } @numbers
The swap trick wins because it avoids a second pass through the list.
Part 4: CASE-INSENSITIVE SORTING
Perl sorts strings case-sensitively by default:
my @words = qw(apple Banana cherry APRICOT);
print join(", ", sort @words);
Output:
APRICOT, Banana, apple, cherry
Uppercase letters carry lower ASCII codes than lowercase letters, so "A" (code 65) sorts before "a" (code 97).
You can force case-insensitive ordering by comparing the lowercase form of each string:
sort { lc($a) cmp lc($b) } @words
Output:
apple, APRICOT, Banana, cherry
The comparison runs on the lowered strings, but the original casing stays intact in the final output.
Part 5: THE $a AND $b MYSTERY
Where do $a and $b actually come from? They are **package variables** that sort creates on the fly, so you never need to declare them.
The catch is that they live at package scope, not lexical scope, and that distinction matters when you enable
use strict:
use strict;
my @sorted = sort { $a <=> $b } @nums; # Works fine
Perl special-cases $a and $b so strict does not warn about them.
Do not declare your own lexical version inside the same scope:
my $a = 5; # Shadows the sort variables!
sort { $a <=> $b } @nums; # Broken
Your lexical $a hides the magic one, and the comparison breaks.
Part 6: SORTING BY EXTRACTED KEYS
You can sort by any value you can compute from the list elements.
Here is how to sort a list of URLs by the title that follows the pipe character:
my @urls = (
'https://example.com | Example Site',
'https://perl.org | The Perl Foundation',
'https://cpan.org | CPAN',
);
my @sorted = sort {
my ($title_a) = $a =~ /\| (.+)$/;
my ($title_b) = $b =~ /\| (.+)$/;
lc($title_a) cmp lc($title_b);
} @urls;
The regex \| (.+)$ captures everything after the pipe, and we compare those extracted titles.
Output:
https://cpan.org | CPAN
https://example.com | Example Site
https://perl.org | The Perl Foundation
Part 7: THE SCHWARTZIAN TRANSFORM
The URL sort above runs the regex twice for every comparison. Since sort performs O(n log n) comparisons, a large list will repeat that regex work many times.
The **Schwartzian Transform** solves that by computing the sort key once per element:
my @sorted =
map { $_->[0] }
sort { $a->[1] cmp $b->[1] }
map { [ $_, /\| (.+)$/ ] }
@urls;
Read the chain from bottom to top:
1. map: Transform each URL into [$url, $title]
2. sort: Compare by $title (the second element)
3. map: Extract just the URL (first element)
Each regex now runs exactly once per element instead of once per comparison, which pays off quickly on large datasets.
Part 8: BREAKING IT DOWN
Here is a step-by-step trace of the transform:
INPUT: @urls = (
'https://example.com | Example Site',
'https://perl.org | The Perl Foundation',
'https://cpan.org | CPAN',
)
STEP 1 - First map (bottom):
[
['https://example.com | Example Site', 'Example Site'],
['https://perl.org | The Perl Foundation', 'The Perl Foundation'],
['https://cpan.org | CPAN', 'CPAN'],
]
STEP 2 - Sort by index [1]:
[
['https://cpan.org | CPAN', 'CPAN'],
['https://example.com | Example Site', 'Example Site'],
['https://perl.org | The Perl Foundation', 'The Perl Foundation'],
]
STEP 3 - Final map (top):
(
'https://cpan.org | CPAN',
'https://example.com | Example Site',
'https://perl.org | The Perl Foundation',
)
The key insight is **compute once, compare many times**.
Part 9: SORTING COMPLEX DATA
You can sort hash keys by their values:
my %scores = (
alice => 87,
bob => 92,
carol => 85,
);
my @ranked = sort { $scores{$b} <=> $scores{$a} } keys %scores;
We sort the keys, but the comparison looks up the corresponding values. Putting $b first in the spaceship operator flips the order to descending, so the highest score lands first.
Output:
bob, alice, carol
Part 10: MULTI-KEY SORTING
You can chain multiple criteria with the or operator:
my @people = (
{ name => 'Alice', age => 30 },
{ name => 'Bob', age => 25 },
{ name => 'Alice', age => 25 },
);
my @sorted = sort {
$a->{name} cmp $b->{name}
or
$a->{age} <=> $b->{age}
} @people;
The block first compares names, then falls back to age. The **or short-circuits**: if the name comparison returns non-zero, Perl stops there. It only checks age when the names match and the comparison returns zero.
Part 11: CUSTOM COMPARISON FUNCTIONS
You can extract complex sort logic into a named subroutine:
sub by_domain {
my ($domain_a) = $a =~ m{://([^/]+)};
my ($domain_b) = $b =~ m{://([^/]+)};
return lc($domain_a) cmp lc($domain_b);
}
my @sorted = sort by_domain @urls;
Notice that you drop the block braces when you pass a named function. The $a and $b magic variables still work inside that subroutine.
Part 12: GOTCHAS
Do not modify $a or $b inside the comparison block:
sort { $a++ <=> $b } # BAD: modifies source array
Your comparison function should be **pure**, meaning it produces no side effects.
Do not return non-numeric values:
sort { 'apple' } # Meaningless
The return value must be numeric, so sort can decide ordering.
Watch out for
undef values:
sort { $a <=> $b } (1, undef, 3); # Warnings!
You can handle missing values with the //` operator:
sort { ($a // 0) <=> ($b // 0) } @list;
Part 13: THE COMPLETE TOOLKIT
# Numeric ascending sort { $a <=> $b } @nums # Numeric descending sort { $b <=> $a } @nums # String ascending sort { $a cmp $b } @strings # String descending sort { $b cmp $a } @strings # Case-insensitive sort { lc($a) cmp lc($b) } @strings # By hash value sort { $hash{$a} <=> $hash{$b} } keys %hash # Multi-key sort { $a->{x} cmp $b->{x} or $a->{y} <=> $b->{y} } @list # Schwartzian (compute once) map { $_->[0] } sort { $a->[1] <=> $b->[1] } map { [$_, expensive_computation($_)] } @list
perl.gg+---------+ | UNSORTED| | 3 1 4 1 | | 5 9 2 6 | +----+----+ | V .-----. / sort \ | <=> | \ / '-----' | V +---------+ | SORTED | | 1 1 2 3 | | 4 5 6 9 | +---------+