Arrays and Lists
9.8 Map, Filter and Reduce
9.8 Map, Filter and Reduce
When traversing the elements of an array (or a list) so far, we have processed so far one item at a time with a loop. We will now study ways to process all the elements in one go.
9.8.1 Reducing a List to a Value
To add up all the numbers in a list, you can use aforloop like this:
sub add_all (@numbers) { my $total = 0;
for @numbers -> $x {
$total += $x;
}return $total;
}
$total is initialized to 0. Each time through the loop,$xgets one element from the list and is added to$total. As the loop runs,totalaccumulates the sum of the elements; a variable used this way is sometimes called anaccumulator.
An operation like this that combines a sequence of elements into a single value is often called a reduction operation because its effect is toreduceall the items to one element (this is also sometimes called “folding” in some other programming languages). These ideas are derived from functional programming languages such as LISP (whose name stands for
“list processing”).
Raku has areducefunction, which generates a single "combined" value from a list of val- ues, by iteratively applying to each item of a list a function that knows how to combine two values. Using thereducefunction to compute the sum of the first ten numbers might look like this:
> my $sum = reduce { $^a + $^b }, 1..10;
55
Remember thefactorialfunction of Section 4.10? It used aforloop to compute the prod- uct of thenfirst integers up to a limit. It could be rewritten as follows using thereduce function:
sub factorial (Int $num) {
return reduce { $^a * $^b }, 1..$num;
}say factorial 10; # -> 3628800
In fact, the code to compute the factorial is so short with thereducefunction that it may be argued that it has become unnecessary to write a subroutine for that. You could just
“inline” the code:
my $fact10 = reduce { $^a * $^b }, 1..10; # -> 3628800
We can do many more powerful things with that, but we’ll come back to that later, as it requires a few syntactic features that we haven’t seen yet.
9.8.2 The Reduction Metaoperator
Raku also has a reduction operator, or rather a reductionmetaoperator. An operator usually works on variables or values; a metaoperator acts on other operators. Given a list and an operator, the[...]metaoperator iteratively applies the operator to all the values of the list to produce a single value.
For example, the following also prints the sum of all the elements of a list:
say [+] 1, 2, 3, 4; # -> 10
This basically takes the first two values, adds them up, and adds the result to the next value, and so on. Actually, there is a form of this operator, with a backslash before the operator, which also returns the intermediate results:
say [\+] 1, 2, 3, 4; # -> (1 3 6 10)
This metaoperator can be used to transform basically any associative infix operator2into a list operator returning a single value.
The factorial function can now be rewritten as:
sub fact(Int $x){
[*] 1..$x;
}my $factorial = fact(10); # -> 3628800
The reduction metaoperator can also be used with relational operators to check whether the elements of an array or a list are in the correct numerical or alphabetical order:
say [<] 3, 5, 7; # -> True say [<] 3, 5, 7, 6; # -> False say [lt] <a c d f r t y>; # -> True
9.8.3 Mapping a List to Another List
Sometimes you want to traverse one list while building another. For example, the following function takes a list of strings and returns a new list that contains capitalized strings:
sub capitalize_all(@words){
my @result;
push @result, $_.uc for @words;
return @result;
}
my @lc_words = <one two three>;
my @all_caps = capitalize_all(@lc_words); # -> [ONE TWO THREE]
@resultis declared as an empty array; each time through the loop, we add the next ele- ment. So@resultis another kind of accumulator.
An operation likecapitalize_allis sometimes called amapbecause it “maps” a function (in this case theucmethod) to each of the elements in a sequence.
Raku has amapfunction that makes it possible to do that in just one statement:
2Aninfixoperator is an operator that is placed between its two operands.
9.8. Map, Filter and Reduce 153 my @lc_words = <one two three>;
my @all_caps = map { .uc }, @lc_words; # -> [ONE TWO THREE]
Here, the mapfunction applies the ucmethod to each item of the @lc_wordsarray and returns them into the@all_capsarray. More precisely, themapfunction iteratively assigns each item of the@lc_wordsarray to the$_topical variable, applies the code block following themapkeyword to$_in order to create new values, and returns a list of these new values.
To generate a list of even numbers between 1 and 10, we might use the range operator to generate numbers between 1 and 5 and usemapto multiply them by two:
my @evens = map { $_ * 2 }, 1..5; # -> [2 4 6 8 10]
Instead of using the$_topical variable, we might also use a pointy block syntax with an explicit iteration variable:
my @evens = map -> $num { $num * 2 }, 1..5; # -> [2 4 6 8 10]
or an anonymous block with a placeholder variable:
my @evens = map { $^num * 2 }, 1..5; # -> [2 4 6 8 10]
Instead of a code block, the first argument to mapcan be a code reference (a subroutine reference):
sub double-sq-root-plus-one (Numeric $x) { 1 + 2 * sqrt $x;
}my @results = map &double-sq-root-plus-one, 4, 16, 42;
say @results; # -> [5 9 13.9614813968157]
The subroutine name needs to be prefixed with the ampersand sigil to make clear that it is a parameter tomapand not a direct call of the subroutine.
If the name of the array on the left side and on the right side of the assignment is the same, then the modification seems to be made “in place,” i.e., it appears as if the original array is modified in the process.
This is an immensely powerful and expressive function; we will come back to it later.
9.8.4 Filtering the Elements of a List
Another common list operation is to select some elements from a list and return a sublist.
For example, the following function takes a list of strings and returns a list that contains only the strings containing a vowel:
sub contains-vowel(Str $string) {
return True if $string ~~ /<[aeiouy]>/;
}
sub filter_words_with_vowels (@strings) { my @kept-string;
for @string -> $st {
push @kept-string, $st if contains-vowel $st;
}
return @kept-string;
}
contains-vowelis a subroutine that returnsTrueif the string contains at least one vowel (we consider “y” to be a vowel for our purpose).
Thefilter_words_with_vowelssubroutine will return a list of strings containing at least one vowel.
An operation likefilter_words_with_vowelsis called afilterbecause it selects some of the elements and filters out the others.
Raku has a function calledgrepto do that in just one statement:
my @filtered = grep { /<[aeiouy]>/ }, @input;
The name of thegrepbuilt-in function used to filter some input comes from the Unix world, where it is a utility that filters the lines that match a given pattern from an input file.
In the code example above, all of@inputstrings will be tested against thegrepblock, and those matching the regex will go into thefilteredarray. Just likemap, thegrepfunction iteratively assigns each item of the@inputarray to the$_topical variable, applies the code block following thegrepkeyword to$_, and returns a list of the values for which the code block evaluates to true. Here, the code block is a simple regex applied to the$_variable.
Just as formap, we could have used a function reference as the first argument togrep:
my @filtered = grep &contains-vowel, @input;
To generate a list of even numbers between 1 and 10, we might use the range operator to generate numbers between 1 and 10 and usegrepto filter out odd numbers:
my @evens = grep { $_ %% 2 }, 1..10; # -> [2 4 6 8 10]
As an exercise, write a program usingmapto produce an array containing the square of the numbers of the input list and a program usinggrepto keep only the numbers of an input list that are perfect squares. Solution: A.7.3.
Most common list operations can be expressed as a combination ofmap,grep, andreduce.
9.8.5 Higher Order Functions and Functional Programming
Besides their immediate usefulness, thereduce,mapandgrepfunctions we have been us- ing here do something qualitatively new. The arguments to these functions are not just data: their first argument is a code block or a function. We are not only passing to them the data that they will have to use or transform, but we are also passing the code that will process the data.
The reduce, map, and grep functions are what are often called higher-order functions, functions that manipulate not only data, but also other functions. These functions can
9.9. Fixed-Size, Typed and Shaped Arrays 155