Day 17 – A loop that bears repeating

I don’t believe anyone has ever Advent blogged about this topic. But it’s getting hard to tell, what with almost six years of old posts. 😁

This story starts in Perl 5, where we implement a countdown with a simple while loop:

$ perl -wE'my $c = 5; while ($c--) { say $c }'
4
3
2
1
0

And of course, if we want to exit early from the loop, we use the trusty last:

$ perl -wE'my $c = 5; while ($c--) { say $c; last if $c == 2 }'
4
3
2

So far, so good. But notice that $c is decremented before each run of the loop body. Under some circumstances it makes more sense to do it afterwards. In Perl 5, we’d use do ... while:

$ perl -wle'my $c = 5; do { print $c; } while $c--'
5
4
3
2
1
0

Great. Now we just need to combine this with the early exit:

$ perl -wle'my $c = 5; do { print $c; last if $c == 2 } while $c--'
5
4
3
2
Can't "last" outside a loop block at -e line 1.

Eh? Oops.

Experienced Perl people will know what’s going on here. The documentation, in this case perldoc perlsyn, explains it clearly:

The “while” and “until” modifiers have the usual “”while” loop” semantics (conditional evaluated first), except when applied to a “do”-BLOCK […], in which case the block executes once before the conditional is evaluated.

…and then…

Note also that the loop control statements described later will NOT work in this construct, because modifiers don’t take loop labels. Sorry.

In other words, the do ... while construct is a little bit of a cheat in Perl 5, made to work as we expect, but really just a do block with reindeer horns glued on top of its head. The cheat is exposed when we try to next and last from the do block.

perldoc perlsyn goes on to explain how you can mitigate this with more blocks and loop labels, but in a way, the damage is already done. We’ve lost a little bit of that natural belief in goodness, and Santa, and predictable language semantics.

It goes without saying that this is a situation up with which Perl 6 will not put. Let’s see what Perl 6 provides us with.

Instead of painstakingly lining up differences, let’s just first replay our Perl 5 session with the corresponding Perl 6:

$ perl6 -e'my $c = 5; while $c-- { say $c }'
4
3
2
1
0
$ perl6 -e'my $c = 5; while $c-- { say $c; last if $c == 2 }'
4
3
2
$ perl6 -e'my $c = 5; repeat { say $c } while $c--'
5
4
3
2
1
0
$ perl6 -e'my $c = 5; repeat { say $c; last if $c == 2 } while $c--'
5
4
3
2

We’ve gotten rid of a few parentheses, as is usually the case when changing to Perl 6. But the biggest difference is that do has now been replaced by another keyword repeat.

In Perl 5, when we saw the do keyword and a block, we didn’t know if there would come a while or until statement modifier after the block. In Perl 6, when we see the repeat, that’s a promise that this is a repeat ... while or repeat ... until loop. So it’s a real loop in Perl 6, not a fake.

Oh, and the last statement works! As it does in real loops.


The story might have ended there, but Perl 6 has a little bit more of a gift to give with this type of loop.

Let’s say we’re waiting for an input that has to have exactly five letters.

sub valid($s) {
    $s ~~ /^ <:Letter> ** 5 $/;
}

This is the type of thing that we’d tend to express with a repeat-style loop, because we have to read the input first, and then find out if we’re done or not:

my $input;
repeat {
    $input = prompt "Input five letters: ";
} until valid($input);

The $input variable has to be declared separately outside of the loop block, because we’re testing it in the until condition outside of the loop block.

Even though we now know to expect a while or until after the block, having that information there makes the end-weight of the loop a bit problematic: we’re delaying some of the most pertinent information until last.

(A similar problem happens in Perl 5 regexes, where a lot of modifiers can show up after the final /, changing the meaning of the whole regex body. And of course, even in spoken and written language, you might have a sentence which goes on and on and just won’t stop, even though it should have long ago, and finally when you think you’ve got the hang of it, it surprises you unduly by ending with altogether the wrong hamster.)

For this reason, Perl 6 allows the following variant of the above repeat loop:

my $input;
repeat until valid($input) {
    $input = prompt "Input five letters: ";
}

That looks nicer!

I hasten to underline that even after moving the condition to the top of the loop like this, the code still has the previous behavior — that is, the condition valid($input) is still evaluated after each iteration of the loop body. (That is, after all, the difference between a repeat until loop and just an until loop.) In other words, we get to place the condition where it’s more prominent and harder to miss, but we retain the expected eval-condition-afterwards semantics.

As a final nice bonus, we can now inline the declaration of $input into the loop condition itself.

repeat until valid(my $input) {
    $input = prompt "Input five letters: ";
}

This clearly shows the difference between order of elaboration (in which variables are declared before they are used) and order of execution (in which statements and expressions evaluate in the order they damn well please).

That’s repeat, folks. Solves a slew of problems, and lets us write nice, idiomatic code.

5 thoughts on “Day 17 – A loop that bears repeating

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s