Get out early with Perl statement modifiers

When I first start­ed writ­ing Perl in my ear­ly 20’s, I tend­ed to fol­low a lot of the struc­tured pro­gram­ming con­ven­tions I had learned in school through Pascal, espe­cial­ly the notion that every func­tion has a sin­gle 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 pret­ty con­vo­lut­ed, espe­cial­ly if I was doing some­thing like val­i­dat­ing mul­ti­ple argu­ments. And at the time I didn’t yet grok how to han­dle excep­tions 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 bal­anced? This is some­times called the arrow anti-​pattern, with the arrowhead(s) being the most nest­ed state­ment. The default ProhibitDeepNests perlcritic pol­i­cy is meant to keep you from doing that.

The way out (lit­er­al­ly) is guard claus­es: check­ing ear­ly if some­thing is valid and bail­ing out quick­ly if not. The above exam­ple 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 state­ment mod­i­fiers (some­times called post­fix con­trols) 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 lat­ter can be con­fus­ing with double-​negatives.)

Guard claus­es aren’t lim­it­ed to the begin­nings of func­tions or even exit­ing func­tions entire­ly. Often you’ll want to skip or even exit ear­ly con­di­tions in a loop, like this exam­ple that process­es files from stan­dard input or the com­mand line:

while (<>) {
    next if /^SKIP THIS LINE: /;
    last if /^END THINGS HERE$/;

    ...
}

Of course, if you are val­i­dat­ing func­tion argu­ments, you should con­sid­er using actu­al sub­rou­tine sig­na­tures if you have a Perl new­er than v5.20 (released in 2014), or one of the oth­er type val­i­da­tion solu­tions if not. Today I would write that postal func­tion like this, using Type::Params for val­i­da­tion 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 larg­er pro­gram, I’d wrap that print_postal_address call in a try block and catch excep­tions such as those thrown by the code ref­er­ence $check gen­er­at­ed by compile_named. This high­lights one con­cern of guard claus­es and oth­er return ear­ly” pat­terns: depend­ing on how much has already occurred in your pro­gram, you may have to per­form some resource cleanup either in a catch block or some­thing like Syntax::Keyword::Try’s finally block if you need to tidy up after both suc­cess and failure.


Discover more from The Phoenix Trap

Subscribe to get the latest posts sent to your email.

Mark Gardner Avatar

Hi, I’m Mark.

Hi, I’m Mark Gard­ner, and this is my personal blog. I show software developers how to level up by building production-ready things that work. Clear code, real projects, lessons learned.

Comments

7 responses to “Get out early with Perl statement modifiers”

  1. Dave Hodgkinson Avatar

    Well done on avoid­ing unless. perl­crit­ic thanks you.

    1. grinnz Avatar

      I find if !$foo hard­er to process. The only rea­son for that pol­i­cy is to avoid dou­ble neg­a­tives, but that’s not what it enforces. Like many core poli­cies it is a poor imple­men­ta­tion of a niche preference.

  2. Mutant Rob Avatar
    Mutant Rob

    There’s no dou­ble neg­a­tive in the con­di­tion­als. That’s a sim­ple enough use case for unless.

    1. Mark Gardner Avatar

      Fair, but if I’m using perl­crit­ic I have to either anno­tate it with ## no critic or change my config.

      1. grinnz Avatar

        Which is a great argu­ment for why this pol­i­cy should­n’t be used by default, but not much else.

  3. Matt S Trout Avatar
    Matt S Trout

    The perl­crit­ic default poli­cies have quite a num­ber of things that have no rel­e­vance to perl as it’s writ­ten in pro­duc­tion and the for­bid­ding unless is absolute­ly one of them and should be dis­abled — perl crit­ic’s default poli­cies should be some­thing you go through and pick which ones to turn on, the exam­ple con­fig­u­ra­tion is ter­ri­ble.

    Avoiding actu­al dou­ble neg­a­tives in unless is absolute­ly a great idea, though.

    But

    return unless my $user = $rs->find($user_id);

    and sim­i­lar con­structs as a gau­rd clause are absolute­ly idiomat­ic and per­fect­ly fine perl.

    I went for a quick look and found vari­a­tions of this pat­tern in:

    Carton
    Dancer2
    DBIx::Class
    IO::Async
    Mojolicious
    Moo

    so while there’s not nec­es­sar­i­ly a con­sen­sus — and you should absolute­ly make your own choic­es for your own code­bas­es — it’s well worth treat­ing it as a per­fect­ly accept­able option.

    (though as well as avoid­ing dou­ble neg­a­tives, please be aware that if you use unless/​else except for pur­pos­es of trolling you’ll prob­a­bly annoy the crap out of absolute­ly everybody 😉

    – mst

  4. Peter CS Avatar
    Peter CS

    I am rather of the view that you should (if pos­si­ble) val­i­date all the input before return­ing. Something like this:

    $errors = ”;
    $errors .= no name\n” unless $name;
    $errors .= no street 1\n” unless $street1;

    return $errors if $errors;

To respond on your own website, enter the URL of your response which should contain a link to this post's permalink URL. Your response will then appear (possibly after moderation) on this page. Want to update or remove your response? Update or delete your post and re-enter your post's URL again. (Find out more about Webmentions.)