Perl Part I · Module 5 · Arrays and Lists
grep and map
These two functions do what a loop would, in one expression, and they are everywhere in Perl you will inherit. One takes a list and keeps the elements matching a condition; the other takes a list and replaces every element with something computed from it. Learning them is quick. The detail that matters is what the loop variable inside the block actually is, because it is not a copy — it is an alias to the element in the original list. Read through it and everything behaves as you expect. Assign to it and you have silently rewritten the array you were reading, with no warning, in a function whose name promises a transformation rather than a modification.
Foundation15 min readPerlPart IFundamentalsArraysIdiomsDebugging
Part I · Module 5 · Chapter 5.10 · grep and map
1. One Keeps, One Transforms
use strict;
use warnings;
my @nums = (1, 2, 3, 4, 5, 6);
my @over = grep { $_ > 3 } @nums;
my @doubled = map { $_ * 2 } @nums;
printf "grep : %s\n", join(" ", @over);
printf "map : %s\n", join(" ", @doubled);grep : 4 5 6
map : 2 4 6 8 10 12grep { CONDITION } @listreturns the elements for which the block is true. Six in, three out.map { EXPRESSION } @listreturns what the block produced for each element. Six in, six out.
Inside the block, the current element is in $_ — the default variable from Chapter 2.12 §4. That is the whole interface.
Both are the loop you would otherwise write:
my @over;
for my $n (@nums) {
push @over, $n if $n > 3;
}Four lines against one. The one-line form is preferred when the block is short, because the intent — filter this list — is visible at the start of the line rather than assembled from a loop and a conditional push.
2. grep in Scalar Context Is a Count
use strict;
use warnings;
my @status = ("PASS", "FAIL", "PASS", "PASS");
my @passing = grep { $_ eq "PASS" } @status;
my $n = grep { $_ eq "PASS" } @status;
printf "list : %s\n", join(" ", @passing);
printf "scalar : %d of %d\n", $n, scalar(@status);list : PASS PASS PASS
scalar : 3 of 4The same grep, assigned two ways. In list context you get the matching elements; in scalar context you get how many matched.
That is Chapter 5.7's rule applied to a function's return value, and it makes the counting idiom short:
use strict;
use warnings;
my @names = ("alu", "fifo", "decoder");
my $failed = grep { $_ eq "FAIL" } ("PASS", "FAIL", "FAIL");
printf "failures counted : %d\n", $failed;
printf "is fifo present : %s\n", (grep { $_ eq "fifo" } @names) ? "yes" : "no";
printf "is cpu present : %s\n", (grep { $_ eq "cpu" } @names) ? "yes" : "no";failures counted : 2
is fifo present : yes
is cpu present : noThe membership test works because a condition is scalar context, so grep returns a count, and 0 is the only false count — Chapter 2.8. Chapter 5.11 takes these idioms further.
3. map Is Not One-to-One
The name suggests one output per input. It does not work that way, and both directions are useful.
It can expand
use strict;
use warnings;
my @a = (1, 2);
my @pairs = map { ($_, $_ * 10) } @a;
printf "count=%d [%s]\n", scalar(@pairs), join(" ", @pairs);count=4 [1 10 2 20]Two elements in, four out. The block returned a two-element list for each input, and all of those were collected into one flat result.
That flattening is Chapter 5.12's subject, and it is what makes the expansion work.
It can filter
use strict;
use warnings;
my @a = (1, 2, 3, 4);
my @odd = map { $_ % 2 ? $_ : () } @a;
printf "count=%d [%s]\n", scalar(@odd), join(" ", @odd);count=2 [1 3]Returning the empty list () contributes nothing to the result, so the even numbers vanished. map just did grep's job.
That is worth knowing because you will see it, and worth not writing: if you are filtering, grep { $_ % 2 } says so. Use the () return when you need to filter and transform in one pass — which is a real case, and the only one where it is clearer than two steps.
4. $_ Is an Alias, Not a Copy
Here is the behaviour to take seriously.
use strict;
use warnings;
my @safe = ("a", "b", "c");
my @upper = map { uc $_ } @safe;
printf "read only : orig=[%s] result=[%s]\n", join(" ", @safe), join(" ", @upper);
my @unsafe = ("a", "b", "c");
my @also = map { $_ = uc $_ } @unsafe;
printf "assigned : orig=[%s] result=[%s]\n", join(" ", @unsafe), join(" ", @also);read only : orig=[a b c] result=[A B C]
assigned : orig=[A B C] result=[A B C]Read the second line. @unsafe is now [A B C]. The source array was rewritten.
The two blocks differ by three characters:
{ uc $_ }reads$_and returns a new value. The original is untouched.{ $_ = uc $_ }assigns to$_. Since$_is an alias to the element in the source list, assigning to it writes through to the original array.
Nothing on STDERR. Both blocks produce the same returned list, so the only visible difference is in the array you thought you were only reading.
grep behaves the same way:
use strict;
use warnings;
my @names = ("x", "y");
my @kept = grep { $_ = uc $_; 1 } @names;
printf "orig=[%s]\n", join(" ", @names);orig=[X Y]5. The Expression Form
Both functions have a second syntax without braces:
use strict;
use warnings;
my @names = ("alu", "fifo", "decoder");
my @long_block = grep { length($_) > 3 } @names;
my @long_expr = grep length($_) > 3, @names;
printf "block form : %s\n", join(" ", @long_block);
printf "expression form : %s\n", join(" ", @long_expr);block form : fifo decoder
expression form : fifo decoderBoth syntaxes gave the same two names. alu is three characters, which is not greater than three, so it was correctly dropped by both.
The difference is only notation:
- The block form takes a block in braces and no comma after it.
- The expression form takes an expression and a comma before the list.
Use the block form. It scales to a multi-statement condition, it is visually distinct from an ordinary function call, and mixing the two forms in one codebase is a small readability tax for no gain. The expression form is here so you recognise it in inherited code.
6. Chaining Them
Because each returns a list, they compose — and they are read right to left.
use strict;
use warnings;
my @rows = ("alu,184,177", "fifo,92,92", "decoder,1240,1198", "regfile,300,271");
my @low = map { (split /,/, $_)[0] }
grep { my @f = split /,/, $_; $f[2] / $f[1] < 0.95 }
@rows;
printf "below 95%%: %s\n", join(", ", @low);below 95%: regfileRead it from the bottom: take @rows, keep those whose pass rate is under 0.95, then extract the name from each. One expression, and the pipeline order on the page is the reverse of the reading order — which is the one genuine cost of chaining.
If a chain needs more than two stages, name the intermediate results instead:
use strict;
use warnings;
my @rows = ("alu,184,177", "fifo,92,92", "decoder,1240,1198", "regfile,300,271");
my @failing_rows = grep { my @f = split /,/, $_; $f[2] / $f[1] < 0.95 } @rows;
my @failing_names = map { (split /,/, $_)[0] } @failing_rows;
printf "below 95%%: %s\n", join(", ", @failing_names);below 95%: regfileTwo lines, two names, and each stage is printable when something looks wrong. For anything a colleague will maintain, that is the better trade.
7. Why This Matters in Engineering Work
Filtering a result set is the commonest use. "Which tests failed", "which paths have negative slack", "which cells are above the area budget" — all grep with a condition, and all one line.
Counting without a loop. my $n = grep { ... } @list is the count, and if (grep { ... } @list) is the membership test. Both read as the question being asked.
Extracting one field from every row is map { (split /,/, $_)[N] }, which combines this chapter with Chapter 5.6 §5's list slice.
And the alias hazard is a real bug with a long fuse. A map that rewrites its input array produces correct output and corrupt state, and the corruption surfaces wherever that array is read next. Reviewing a map block for an = on $_ takes two seconds and is worth doing, because use warnings will not tell you.
8. Common Mistakes
Assigning to $_ inside the block. It writes through to the source array, silently. The block should return a value.
Expecting map to return one element per input. It returns whatever each block returned, flattened — so it can expand or filter.
Using map to filter when grep would say it. Reserve the () return for filter-and-transform in one pass.
Forgetting grep's scalar context gives a count. That is the feature, not a surprise — but my $x = grep {...} is a number, not a list.
Mixing the block and expression forms. Pick the block form.
Chaining three or more stages. Read right to left is tolerable for two; name the stages beyond that.
Splitting inside both a grep and a map in the same chain. You are parsing each row twice. Fine for small lists; name the stages and split once when it matters.
9. Exercise
Exercise 1 — both functions. From (1 .. 10), use grep to keep the values above 6 and map to produce each value squared. Report both lists and their counts.
Exercise 2 — the count. Given eight status strings of which five are PASS, get the count of passes in one expression, then write the membership test for whether any TIMEOUT is present.
Exercise 3 — the alias, measured. Build an array of three lowercase names. Uppercase them with map { uc $_ } and print the original. Then do it again with map { $_ = uc $_ } and print the original. Record both, confirm STDERR is empty, and say which three characters caused the difference.
Exercise 4 — the same hazard in a loop. Modify an array in place with for my $v (@a) { $v = uc $v }. Confirm the array changed. Then say in one clause when you would write that deliberately and what you would add to the line.
Exercise 5 — expand. Use map to turn ("a", "b") into ("a", "A", "b", "B") in one expression. Report the element count.
Exercise 6 — filter with map, then with grep. Keep the even numbers from (1 .. 8) twice: once with map returning (), once with grep. Confirm both give the same list and say which you would commit.
Exercise 7 — a chain, then named stages. From four rows of name,total,passed, produce the names whose pass rate is below 1.0, first as a single map/grep chain and then as two named steps. Say which you would hand to a colleague and why.
10. Summary
grep { COND } @listkeeps the matching elements;map { EXPR } @listreturns what the block produced for each.- The current element is in
$_inside both blocks. grepin scalar context returns the count, which makes counting and membership tests one-liners.mapis not one-to-one. A block returning several values expands the result; a block returning()filters.$_is an alias to the original element. Reading it is safe; assigning to it rewrites the source array, silently.- A
foreachloop variable is an alias too — occasionally useful deliberately, always worth a comment. - The block form and the expression form are equivalent. Prefer the block.
- Chains read right to left. Two stages is fine; name the intermediates beyond that.
Chapter 5.11 collects the short answers to three questions this chapter made possible — is it in there, how many match, and what does it look like reversed — including the one where reverse does something completely different depending on the context it is called in.
Where this fits
Part of the Perl curriculum.
