Strangely Consistent

Musings about programming, Perl 6, and programming Perl 6

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