Get Out Early With Perl Statement Modifiers
Learn how to avoid deeply-nested conditions in your code with guard clauses using Perl’s statement modifiers.
Join the DZone community and get the full member experience.
Join For FreeWhen I first started writing Perl in my early 20’s, I tended to follow a lot of the structured programming conventions I had learned in school through Pascal, especially the notion that every function has a single point of exit. For example:
sub double_even_number {
# not using signatures, this is mid-1990's code
my $number = shift;
if (not $number % 2) {
$number *= 2;
}
return $number;
}
This could get pretty convoluted, especially if I was doing something like validating multiple arguments. And at the time I didn’t yet grok how to handle exceptions with eval
and die
, so I’d end up with code like:
sub print_postal_address {
# too many arguments, I know
my ($name, $street1, $street2, $city, $state, $zip) = @_;
# also this notion of addresses is naive and US-centric
my $error;
if (!$name) {
$error = 'no name';
}
else {
print "$name\n";
if (!$street1) {
$error = 'no street';
}
else {
print "$street1\n";
if ($street2) {
print "$street2\n";
}
if (!$city) {
$error = 'no city';
}
else {
print "$city, ";
if (!$state) {
$error = 'no state';
}
else {
print "$state ";
if (!$zip) {
$error = 'no ZIP code';
}
else {
print "$zip\n";
}
}
}
}
}
return $error;
}
What a mess. Want to count all those braces to make sure they’re balanced? This is sometimes called the arrow anti-pattern, with the arrowhead(s) being the most nested statement. The default ProhibitDeepNests perlcritic
policy is meant to keep you from doing that.
The way out (literally) is guard clauses: checking early if something is valid and bailing out quickly if not. The above example could be written:
sub print_postal_address {
my ($name, $street1, $street2, $city, $state, $zip) = @_;
if (!$name) {
return 'no name';
}
if (!$street1) {
return 'no street1';
}
if (!$city) {
return 'no city';
}
if (!$state) {
return 'no state';
}
if (!$zip) {
return 'no zip';
}
print join "\n",
$name,
$street1,
$street2 ? $street2 : (),
"$city, $state $zip\n";
return;
}
With Perl’s statement modifiers (sometimes called postfix controls) we can do even better:
...
return 'no name' if !$name;
return 'no street1' if !$street1;
return 'no city' if !$city;
return 'no state' if !$state;
return 'no zip' if !$zip;
...
(Why if
instead of unless
? Because the latter can be confusing with double-negatives.)
Guard clauses aren’t limited to the beginnings of functions or even exiting functions entirely. Often you’ll want to skip or even exit early conditions in a loop, like this example that processes files from standard input or the command line:
while (<>) {
next if /^SKIP THIS LINE: /;
last if /^END THINGS HERE$/;
...
}
Of course, if you are validating function arguments, you should consider using actual subroutine signatures if you have a Perl newer than v5.20 (released in 2014), or one of the other type validation solutions if not. Today I would write that postal function like this, using Type::Params for validation and named arguments:
use feature qw(say state);
use Types::Standard 'Str';
use Type::Params 'compile_named';
sub print_postal_address {
state $check = compile_named(
name => Str,
street1 => Str,
street2 => Str, {optional => 1},
city => Str,
state => Str,
zip => Str,
);
my $arg = $check->(@_);
say join "\n",
$arg->{name},
$arg->{street1},
$arg->{street2} ? $arg->{street2} : (),
"$arg->{city}, $arg->{state} $arg->{zip}";
return;
}
print_postal_address(
name => 'J. Random Hacker',
street1 => '123 Any Street',
city => 'Somewhereville',
state => 'TX',
zip => 12345,
);
Note that was this part of a larger program, I’d wrap that print_postal_address
call in a try
block and catch
exceptions such as those thrown by the code reference $check
generated by compile_named
. This highlights one concern of guard clauses and other “return early” patterns: depending on how much has already occurred in your program, you may have to perform some resource cleanup either in a catch
block or something like Syntax::Keyword::Try’s finally
block if you need to tidy up after both success and failure.
Published at DZone with permission of Mark Gardner, DZone MVB. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments