map, grep and sorting
Turning a loop into a single line: changing every item, keeping the ones a condition likes, and putting a list in the order you actually asked for.
Change every item
map runs a small block of code once per item and collects the answers: my @longer = map { length $_ } @words;. The block sees the current item in $_, and whatever the block works out to is what goes into the new list.
The original list is not touched — map answers a new one. That is the whole reason the loop that starts by building an empty array and pushing into it is worth replacing: map says what is happening in one line, with the intent at the front.
Keep the items a condition likes
grep runs a block once per item and keeps the ones where the block is true: my @big = grep { $_ > 20 } @scores;. If map is the transformer, grep is the filter, and they chain naturally — filter first when that saves work, change first when the condition reads more clearly on the changed values.
Both take a block, and both also accept a plain pattern or expression: grep /^a/, @words keeps the words that begin with a. Use the short form when it reads like a sentence and braces when it does not.
Putting a list in order
sort answers a new list in order, and by default it compares the items as text: sort @numbers puts 100 before 60, because the character 1 comes before the character 6. For numbers you have to say so, with a comparator block.
The block is handed two items at a time in the variables $a and $b, and it answers which one comes first: sort { $a <=> $b } @numbers is smallest first, and sort { $b <=> $a } is largest first. For text you use cmp instead of <=>, and sort { $a cmp $b } is the default written out loud — worth writing out loud, so the next reader knows you meant it.
What the block sees, and what it gives back
Both map and grep walk the list in order and put the current item in $_ for the block. map collects whatever the block evaluates to, one result per item, so a block that returns an empty list removes that item from the answer. grep ignores the block's value except for its truth, and keeps the original item when it is true.
The item in $_ is an alias, not a copy. A block that writes to $_ changes the original list, which is useful for trimming every value and surprising when you meant to build a separate one. The functions answer new lists, so assigning the result is what makes the change visible. That is also why a map block that only prints is a mistake: the printed text is not the answer being collected.
The comparator answers a direction
A sort block is called with two items in $a and $b, and it must answer a negative number, zero or a positive number. <=> does that for numbers and cmp does it for text. sort { $a <=> $b } means smallest first; swapping the variables means largest first.
A tie can be broken by putting a second comparison after ||: $a->{'score'} <=> $b->{'score'} || $a->{'name'} cmp $b->{'name'}. Zero is false, so the second comparison only runs when the first says the two are equal. That is how one sort produces a stable, readable order instead of an arbitrary one.
An expression where a block would do
Both functions accept an expression instead of a block when the expression is unambiguous: map length($_), @words and grep /^a/, @words say the same thing as the braced forms. The comma after the expression is what separates it from the list, and it is the part people forget.
Changing, keeping and ordering a list
use strict;
use warnings;
my @scores = (42, 7, 19, 88, 7, 60);
my @doubled = map { $_ * 2 } @scores;
print "doubled: ", join(' ', @doubled), "\n";
my @big = grep { $_ > 20 } @scores;
print "big: ", join(' ', @big), "\n";
print "as text: ", join(' ', sort @scores), "\n";
my @sorted = sort { $a <=> $b } @scores;
print "numbers: ", join(' ', @sorted), "\n";
my @largest = sort { $b <=> $a } @scores;
print "largest: ", join(' ', @largest), "\n";
my @names = ('grace', 'Ada', 'linus');
print "by name: ", join(' ', sort { $a cmp $b } @names), "\n";
my @labels = map { $_ % 2 ? "$_ odd" : "$_ even" } @scores;
print join(', ', @labels), "\n";Six numbers, changed, kept and ordered
Run it and read the two sort lines side by side. Then add a map that turns every number into a label, and a grep that keeps only the numbers under 20.
When a loop builds an array by pushing into it, a map is usually hiding inside: a loop that pushes the doubled value of every item says the same thing as one map line, in more words.
Three list-function mistakes are worth a second look. A bare sort compares as text, so numbers come out in an order like 100 before 60. Declaring my $a or my $b inside a sort block hides the values Perl supplies, so never declare them. And my $n = grep {...} @list puts a count in $n, not the first match; assign to an array when you want the items.
Try these next
- Turn a list of names into lengths with
map, then keep the lengths over four withgrep. - Sort a list of words by length, breaking ties alphabetically.
- Use
mapto trim every value in place, then print the original list to see what changed.
Clean, filter and order a list of names
Run it and check the order. Then add a name with a leading space and a lowercase first letter, and make the map clean both before anything else runs.
Check what you learned
One question at a time. A wrong answer explains itself — take another go.
Part 1 of 5
What does map { uc $_ } @words answer?
