DEV Community

Elizabeth Mattijsen
Elizabeth Mattijsen

Posted on • Edited on

Next / Last / Side-effects

This is part 5 of the "Don't fear the grepper!" series.

The next confession

Yes, another one. In the previous blog post I said that you can decide to not accept some values in a .map, by returning the Empty value from the block.

say (1..12).map({
    if $_ %% 2 {   # is it divisible by 2?
        $_         # yes, accept
    }
    else {         # not divisible by 2
        Empty      # don't accept
    }
}); # (2 4 6 8 10 12)
Enter fullscreen mode Exit fullscreen mode

There's actually another way to decide to not accept a value, and that is by using the next control flow statement. With next, you're actually telling .map to stop executing any code inside the block immediately, and start the next iteration.

So how would that look in the above case?

say (1..12).map({
    next unless $_ %% 2;  # not divisible by 2, next!
    $_                    # accept
}); # (2 4 6 8 10 12)
Enter fullscreen mode Exit fullscreen mode

Note that using next will interrupt the normal flow of the program. When it executes, it will look up the call stack and instruct the first handler capable of handling next to immediately continue with the next iteration. In this case, the .map method has installed such a handler.

And you see I used unless, instead of if not there. I generally use unless only as a statement modifier, because using it with blocks generally doesn't improve readability, and therefor maintainability of any codebase. But of course, I could also have written next if not $_ %% 2!

Other than the program flow interruption feature of next, next is just a subroutine that is provided by the Raku core. So you can have multiple references to next in the same block.

say (1..12).map({
    next unless $_ %% 2;  # not divisible by 2, next!
    next unless $_ %% 3;  # not divisible by 3, next!
    $_                    # accept
}); # (6 12)
Enter fullscreen mode Exit fullscreen mode

By using next you can create quite complicated logic when doing any mapping using .map.

For the last time

Sometimes you want to not accept any more values in a .map when a certain condition fires, for instance when a certain value is seen. Let's take one of the above examples, and make it stop when the value 7 has been encountered:

my $done = False;            # create flag
say (1..12).map({
    $done = True if $_ == 7; # switch flag if appropriate
    next if $done;           # we're done, next!
    next unless $_ %% 2;     # not divisible by 2, next!
    $_                       # accept
}); # (2 4 6)
Enter fullscreen mode Exit fullscreen mode

As you can see, this is a bit of a hassle. Fortunately, the Raku Programming Language has a solution for that in the form of the last control flow statement. So let's rewrite this example using last:

say (1..12).map({
    last if $_ == 7;     # we're done
    next unless $_ %% 2; # not divisible by 2, next!
    $_                   # accept
}); # (2 4 6)
Enter fullscreen mode Exit fullscreen mode

Like next, last will interrupt the normal flow of the program. When it executes, it will look up the call stack and instruct the first handler capable of handling last to stop iterating. In this case, the .map method has installed such a handler.

Wow, that is so much easier!

Side effects

The previous example had one interesting side-effect: setting a flag outside of the block inside the .map. Yes, in the Raku Programming Language you can refer to variables outside of its lexical scope, as long as they are lexically "visible". You can use this feature for instance, to keep a count of even numbers you've seen:

my $seen = 0;             # initialize counter
say (1..12).map({
    next unless $_ %% 2;  # not divisible by 2, next!
    $seen++;              # increment counter
    $_                    # accept
}); # (2 4 6 8 10 12)
say "$seen even numbers"; # 6 even numbers
Enter fullscreen mode Exit fullscreen mode

As you can see, the Raku Programming Language also has a ++ postfix operator for incrementing integer values!

But what if you're only interested in how many even numbers were seen, and not interested in the actual numbers themselves? Well, that should be easy: remove the say, and the final $_ in the block (as we're not interested in the actual value when returning from the block anyway).

my $seen = 0;             # initialize counter
(1..12).map({
    next unless $_ %% 2;  # not divisible by 2, next!
    $seen++;              # increment counter
});
say "$seen even numbers"; # 6 even numbers
Enter fullscreen mode Exit fullscreen mode

And in that case, we might as well make the increment conditional, and lose the next!

my $seen = 0;             # initialize counter
(1..12).map({
    $seen++ if $_ %% 2;   # divisible by 2, increment!
});
say "$seen even numbers"; # 6 even numbers
Enter fullscreen mode Exit fullscreen mode

This has now become a case in which the .map method only executes the given block for its side-effects.

For all we know

Actually, the Raku Programming Language has a better syntax for that: the for control statement:

my $seen = 0;             # initialize counter
for 1..12 {
    $seen++ if $_ %% 2;   # divisible by 2, increment!
}
say "$seen even numbers"; # 6 even numbers
Enter fullscreen mode Exit fullscreen mode

Yes. The for loop in Raku, is basically a .map of which the body is only executed for its side-effects. They both use the same underlying iterator mechanism. Which means that you can use next and last also in for loops, because it is basically a .map (or vice-versa, depending on how you look at it).

The underlying iterator mechanism is material for a whole separate set of blog posts, so I won't go further into that here and now. Suffice to say that Raku attempts to unify many different concepts that appear to be different on the surface, to deeper unifying logic and syntax.

Signature features

Remember that in the first post of this series, we saw that you could create a block taking a value and put it into a specific variable:

-> $number { $number %% 2 }
Enter fullscreen mode Exit fullscreen mode

Would you be able to use that same syntax with for? Yes, you can:

my $seen = 0;                # initialize counter
for 1..12 -> $number {
    $seen++ if $number %% 2; # divisible by 2, increment!
}
say "$seen even numbers";    # 6 even numbers
Enter fullscreen mode Exit fullscreen mode

In fact, the -> $number syntax is a property of the block, not of the .map or the for loop! In fact, that feature is called the signature property of the block. Does this also imply that you can use that syntax in for instance an if statement? Yes, you can:

if complicated-calcution($input) -> $result {
    say "Result of calculation: $result";
}
Enter fullscreen mode Exit fullscreen mode

Because the if statement also just accepts a block, just as .map or for expect a block!

But what about grep?

This series of blog posts has "grep" in its title. So how does this apply to .grep? Could you use .grep for its side-effects? Yes, you could:

my $seen = 0;             # initialize counter
(1..12).grep({
    $seen++ if $_ %% 2;   # divisible by 2, increment!
});
say "$seen even numbers"; # 6 even numbers
Enter fullscreen mode Exit fullscreen mode

But you probably shouldn't. Because .grep is intended to filter out values from a list, and you're not using it for that in this example. And using .grep for its side-effects only will confuse whoever will be maintaining your code in the future! And that could be you!

Conclusion

This concludes the fifth part of the series, this time introducing the next and last loop control flow statements. And hopefully instilled the notion that a for loop is nothing but a .map that is only executed for its side-effects. Also that blocks have signatures, that can be specified in many other situations in Raku code, such as with an if.

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.

Top comments (0)