Don't use until, unless...

In Perl 5, until is a negated version of while. Instead of writing this:

while (defined(my $i = $iter->next())) {
    say $i;
}

Using until allows you to write this:

until (!defined(my $i = $iter->next())) {
    say $i;
}

until is documented under the "Compound Statements" heading of the perlsyn document.

Practice and policy

The thoroughly excellent book Perl Best Practices by Damian Conway (DCONWAY), the Black Wizard of Perl, exhorts us not to use the until control structure at all. The core policy set from the similarly excellent Perl::Critic dutifully implements the book's recommendation by raising low-severity warnings if you use it as a block:

until ($foo) {
    bar();
}

This yields:

"until" block used at line 1, column 1. See page 97 of PBP. (Severity: 2)

If you use it as a postfix control, an entirely different error arises from transgressing a different edict of Perl Best Practices' holy writ:

bar() until $foo;

This yields:

Postfix control "until" used at line 1, column 7. See pages 96,97 of PBP. (Severity: 2)

The rationale given for this practice and its derived policy is that humans don't read this sort of negative or backwards logic too well, especially when combined with double-negatives or complex conditionals, and it's too easy to confuse yourself, especially when you need to add multiple conditions.

All of that is completely true, and likely hard-won knowledge for experienced programmers, especially those maintaining big complex legacy systems. While the first priority is always to keep your expressions and conditionals simple for readability, it's also important to express them positively whenever you can, avoiding especially the perils of double-negatives:

unless (!$conda || !($condb and !$condc)) {
    doit() until !$condd;
}

The computer doesn't struggle with stuff like that. Humans do. That's why we write things in ways that fit with the way human languages work, so that we can read it without getting confused. That was one of Larry Wall's goals from the very beginning; to use natural language principles in Perl, and I think that's why there's a subtlety to until that gets missed in its ability to clarify intent or goal.

Structure to sentence

Let's look at the way the word until is used in English (the human language, not the Perl module).

This sounds very awkward to me:

Until all the bugs are processed, go through each one, reproduce it as best you can, find the commit that introduced it if you can, and then send the details to the developer.

That really does seem like putting the cart before the horse. You're trying to explain a procedure to someone, and also the point at which repeated applications of the procedure should stop (expressed as a goal), but you're doing it in a really unintuitive order, at least in English.

The example is both grammatically and semantically correct in English, but it feels awkward. If you're a native English speaker, try saying it out loud. Does that give you the same kind of awkward cognitive load you feel when trying to read an until block in Perl? I bet it does. This is why I agree completely that until blocks are a bad idea.

However, in my opinion, the alternative that Perl::Critic would suggest is not actually that much better while we're following this train of thought. Here's the English equivalent of a while block:

While there are any bugs not yet processed, go through each one, reproduce it as best you can, find the commit that introduced it if you can, and then send the details to the developer.

This next one sounds a fair bit less awkward, moving the condition to the very end:

Go through each bug, reproduce it as best you can, find the commit that introduced it if you can, and send the details to the developer, until all the bugs are processed.

But it's still not quite there.

Short postfix until

Here is how I'd actually say it; I'd define the non-trivial procedure and then, in an entirely distinct sentence from that definition, give instructions on how to apply it:

Here's our bug processing routine: pick a bug, reproduce it if you can, find the commit that introduced it if you can, and then send the details to the developer. Do that until all the bugs are processed.

Which, in Perl, might look something like this:

sub process_bug {
    my $bug = shift @bugs;
    my %details;
    $details{reproduce} = reproduce($bug);
    $details{commit} = bisect($bug);
    send(\%details, $developer);
    return;
}
process_bug until !@bugs;

Now, given the abstraction provided by that subroutine, you might instead choose to write that last line as:

process_bug while @bugs;

That's shorter, and no less correct, though Perl::Critic will still be upset with you about the postfix control structure. However, I think that the former expresses your goal and intent slightly more clearly than the latter does. We don't really want to process bugs while there are any bugs. That's the method, not the goal. We want to process the bugs until there are none left to process, so that we can get on with the rest of our lives (or in this case, the rest of the program). We'd express ourselves that way in natural language, and so Perl lets us say it that way, too.

Here's an example that might be more immediately obvious:

while (!eof) {
    read_line;
}

Or, if you prefer:

read_line while !eof;

Neither of those are too tough, and both are a pretty familiar pattern. But do you think this maybe expresses your intent more clearly, closer to how you would do it in natural language?

read_line until eof;

You want to parse lines until you get to the end of the file. You don't want to parse the lines while there are lines to parse. That's not what you want; that's how you get what you want.

But you certainly wouldn't say "until the end of the file, parse lines":

until (eof) {
    read_line;
}

That's completely correct, but I feel it's weird to say and weird to read in English.

All this means that I haven't banished until entirely from my code, as long as the procedure to meet the goal described by until can be concisely referenced (i.e. it's wrapped in a subroutine) and its conditional simply described or encapsulated, and written after the procedure that will eventually get us there (i.e. a postfix conditional). That may be a somewhat strict set of criteria, but it does come up.

Scaling

When Perl Best Practices asserts that until doesn't scale well, it's hard to argue, for the natural language reasons discussed above. The subtlety that until gives you in goal expression is completely outweighed when you need to add complex conditionals.

This might be OK:

doit() until $foo > 5;

However if you need to add a test to make sure that $bar is false, this gets insane rapidly, and really hard to read:

doit() until ($foo > 5 || $bar);

And you'd do better to write this, exactly as it suggests:

doit() while ($foo <= 5 && !$bar);

That's because you've changed the whole semantics of the structure. You're no longer expressing things in terms of goals, but conditions, so the while loop becomes more appropriate. Maybe you can refactor the $bar condition, so that you can still express it in terms of a goal? Maybe not.

When I use until in the way I would a sentence in natural language, I stop thinking of it as a misfeature. I have similar thoughts about a subtlety of unless that I might discuss in a later post.

3 Comments

I was about to write you some righteously angry comment but you last paragraph was unfortunately what I wanted to express myself. I also stopped using negated form for more than simple expressions because for once they are harder to parse that if/while and there later part is unnatural too which is the opposite for that the keywords were invented.

I will agree with you that unnecessary negation is bad. But I will not agree that `unless` keyword usage is complex.

You should just use it in right place. By `unless` you should mark code which SHOULD NOT RUN in usual case.

Look:

sub process {
my( $self ) = @_;

return unless $self->is_valid; # Read this code as: $self should be valid

my $result = ...;

return $result;
}

Look next examples where I use `unless` with success (And I think code with `unless` here is clear in compare to usual `if`):

$self->error( $tx->res->json ) unless $tx->res->is_success;

Transaction response SHOULD BE success


return $c->render( text => 'We support only HTML and JSON', status => 415 )
unless $f eq 'json' || $f eq 'html';

Format $f SHOULD BE json or html
(Try to rewrite this with usual `if`. I think it will be more complex ;-) )


Another real code:

return unless my $d = $c->stash( 'data' );
return unless $c->app->mode eq 'production';

# There SHOULD BE data in production mode


Even complex conditions are easy to understand. Just keep in mind that: **you go forward if EXPR is true**, so:

return 'IP is not IPv' .$version unless !$version || $ip->version eq $version;

Go forward if $version is not required OR $ip have required $version. We return error in all other cases.

Just remember. you mark your code with until/unless which usually will not work:

until( $x > 0 ) {
$x += 3;
}

$x usually >0 here and you skip { $x += 3 } block. Is this easy to understand? I think, it is!

Because you usually read file the `while( !eof )` is more intuitive.

Just really avoid "unless... else". That's... just.... wrong.

Leave a comment

About Tom Ryder

user-pic Banging out Perl to help keep my corner of the internet from falling over. TEJR on CPAN.