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.
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.