This is part 4 of the "Don't fear the grepper!" series.
Expanding the notion of grep
The grep
method allows one to filter a list of values: either a value gets through, or it does not. In this way, the functionality of grep
is rather limited.
What if you would not only like to filter out unwanted values, but also would like to adapt an acceptable value on the fly? Or turn a single value into multiple values? With the map
method, you can!
The map
method provides a superset of the functionality of grep
. But you can also use it as grep
with a block to do the filtering (instead of using something to smart-match against).
In many ways, understanding map
well, will make understanding a lot of aspects of the Raku Programming Language a lot easier! So let's focus on that a bit.
Using map as grep
Let's go back to the original example of grep
in this series, this time using the topic variable in a block:
say (1..10).grep({ $_ %% 2 }); # (2 4 6 8 10)
You could write this using map
as:
say (1..10).map({
if $_ %% 2 { # is it divisible by 2?
$_ # yes, accept
}
else { # not divisible by 2
Empty # don't accept
}
}); # (2 4 6 8 10)
A little verbose, indeed. But please hang in there!
As we've seen before with blocks, the last value evaluated will be returned by the block. So if the condition $_ %% 2
is True, it will return $_
, else it will return Empty
. Now, what is Empty
, you might ask?
Slipping away
The Raku Programming Language has a number of special values that have special meanings. One of them is Empty
, a Slip
without any elements. A Slip
is a special type of list that automatically flattens in any outer list-like structure.
So if the condition $_ %% 2
is not true, an empty Slip will be put into the result of the map
, and thus not produce a value. So effectively removing the current value of $_
from the list.
But you are not limited to slipping an empty list! You can use the slip
subroutine to create a Slip with any values you give it. For example:
say (1..10).map({
if $_ %% 2 { # divisible by 2?
slip($_ - 0.5, $_ + 0.5) # produce values around it
}
else { # not divisibly by 2
Empty # don't accept
}
}); # (1.5 2.5 3.5 4.5 5.5 6.5 7.5 8.5 9.5 10.5)
In this example the even numbers are replaced by two values, one 0.5 less and one 0.5 more than the topic.
Consequences of not slipping
So, what would happen if you do not slip these values, but just produce the two values?
say (1..10).map({
if $_ %% 2 { # divisible by 2?
$_ - 0.5, $_ + 0.5 # produce list with values
}
else { # not divisible by 2
Empty # don't accept
}
}); # ((1.5 2.5) (3.5 4.5) (5.5 6.5) (7.5 8.5) (9.5 10.5))
You would get a 5-element list of 2-element lists. Instead of a single 10-element list. In other words, the produced lists do not get flattened.
Note that it was not necessary to put parentheses around the 2-element list. In Raku the comma (aka the
,
operator) creates lists, not parentheses!
If that is what you wanted, then be happy. In this blog post, it is not what we wanted, so we used a slip
instead.
Automatic emptying
Using an if
/ else
structure is still pretty verbose though, and yes this can be expressed in a shorter way:
say (1..10).map({
if $_ %% 2 {
slip($_ - 0.5, $_ + 0.5)
}
}); # (1.5 2.5 3.5 4.5 5.5 6.5 7.5 8.5 9.5 10.5)
What? You just removed the else
? Yup! The reason this works, is that the value of a failed if
(or elsif
for that matter), is Empty
. So you don't have to specify the else
clause explicitly!
Still, it feels like this could still be shorter. And you'd be right! Since there is no else
in the code anymore, you can use the "statement modifier" version of if
(sometimes also referred to as "postfix if"):
say (1..10).map({
slip($_ - .5, $_ + .5) if $_ %% 2
}); # (1.5 2.5 3.5 4.5 5.5 6.5 7.5 8.5 9.5 10.5)
That reduces on curly braces, but is also known to not improve readability for some people. To yours truly, it feels very natural language-like. Like everything in life, YMMV!
Helicopter view
Whenever you're coding something to reach a certain goal, you should always think about the way you got to a solution. And think about whether there couldn't be an easier way to reach the same result.
This is not just about efficiency of code execution, but also about whether your code shows the intent clearly, or not. And clear intent will make it easier for you, or anybody else, now or in the future, to grok the source code.
In this case, producing the values 1.5 through 10.5 with an interval of 1, can be done much easier, because with map
we can also change values unconditionally!
say (1..10).map({ $_ + .5 });
# (1.5 2.5 3.5 4.5 5.5 6.5 7.5 8.5 9.5 10.5)
Much simpler, isn't it? It is even so simple that we can use "whatever-currying"!
say (1..10).map(* + .5);
# (1.5 2.5 3.5 4.5 5.5 6.5 7.5 8.5 9.5 10.5)
And there you have a hopefully easy way to grok the use of the map
method in a nutshell!
Conclusion
This concludes the fourth part of the series, this time introducing the map
method. And also introducing the concept of Empty
, and Slip
in general. And also showing that you can have a statement modifier version of if
if you don't need an else
or an elsif
.!
Questions and comments are always welcome. You can also drop into the #raku-beginner channel on Libera.chat, or on Discord if you'd like to have more immediate feedback.
I hope you liked it! Thanks again for reading all the way to the end.