June 23 2011: map and grep
for
loops are great, but sometimes they feel a bit heavy-handed. Here, let me give an example:
my @numbers = 1..10;
my @squares;
for @numbers {
push @squares, $_ * $_;
}
You should be comfortable reading code such as the above by now: we populate the array @squares
based on the contents of @numbers
. The for
loop is just to make sure we're visiting each element of @numbers
once, and the push
adds a new element to @squares
as we do that.
We need to do this kind of "array population" quite a bit, so there's a function to help us do that. It's called map
:
my @numbers = 1..10;
my @squares = map { $_ * $_ }, @numbers;
Ah, that's nicer.
The two pieces of code do the same thing. But the block we pass to map
need only contain the transformation we want to effect. In this case, we want to transform the numbers to their squares. Just as with for
, the inside of the map
block recognizes the topic variable $_
.
What we're passing in to map
is, in fact, a little piece of code. We can highlight this fact by extracting the calculation into a subroutine, and pass in the subroutine to map
:
sub square($n) {
return $n * $n;
}
my @numbers = 1..10;
my @squares = map &square, @numbers;
(Yes, that &
is a fourth sigil — one for referring to functions. Leaving out the &
would call the function before it got a chance to be passed in to map
.)
Functions that accept other functions as arguments (or that return functions) are called "higher-order functions". There's a whole programming paradigm built around them — functional programming. I am not kidding.
There's nothing to prevent you from map
ping from one element to several, by the way:
for map { $_, $_ }, 1..3 {
.say; # "1 1 2 2 3 3"
}
There's another higher-order function that we shall go through today: grep
. Whereas map
translates from one whole list to another, grep
lets you filter elements from a list.
We can start out the same way as with map
, by doing it the long way, with a for
loop:
my @primes;
for 2..100 {
if is_prime($_) {
push @primes, $_;
}
}
say join " ", @primes; # "2 3 5 7 11 13..."
So you see, it only includes a number in the @primes
array if it is_prime
.
With grep
, it's just this:
my @primes = grep { is_prime($_) }, 2..100;
say join " ", @primes; # "2 3 5 7 11 13..."
Or even just passing in the function directly:
my @primes = grep &is_prime, 2..100;
say join " ", @primes; # "2 3 5 7 11 13..."
Oh, the is_prime
function? No, it's not built in. I guess I should define it for you:
sub is_prime($n) {
return ?($n %% none 2 .. $n - 1);
}
[Author's note: go back and add %%
to the "Arithmetic" post, apparently we needed it. :-)
]
So there we have it. map
and grep
are higher order functions that help you write common for
loops in a shorter way. They help you focus on the code that means things and avoid the code that is always the same every time. Which is nice.