DEV Community

Mark Gardner
Mark Gardner

Posted on Originally published at phoenixtrap.com on

Switching up my switches in Perl

Pretty soon after I started writing Perl in 1994, I noticed that it lacked a construct often found in other languages: the switch statement. Not to worry, though—you can achieve the same effect with a cascading series of if-elsif statements, right?

Well, no, you shouldn’t do that, especially if the chain is really long. There’s even a perlcritic policy about it, which suggests that you use given and when instead.

But given and when (and the smartmatch operator they imply, ~~) are still considered experimental, with behavior subject to change. So what’s a responsible developer to do?

The answer is to use the for statement as a topicalizer, which is a fancy way of saying it assigns its expression to $_. You can then use things that act on $_ to your heart’s content, like regular expressions. Here’s an example:

for ($foo) {
    /^abc/ and do {
        ...
        last;
    };
    /^def/ and do {
        ...
        last;
    };
    # FALL THRU
    ...
}
Enter fullscreen mode Exit fullscreen mode

This will cover a lot of cases (haha, see what I did there? A lot of languages use a case statement… oh, never mind.) And if all you’re doing is exact string matching, there’s no need to bring in regexps. You can use a hash as a lookup table:

my %lookup = (
    foo => sub { ... },
    bar => sub { ... },
);
$lookup{$match}->();
Enter fullscreen mode Exit fullscreen mode

EDIT: If every alternative is assigning to the same variable, a ternary table is another possibility. This is a chained set of ternary conditional (? :) operators arranged for readability. I first heard about this technique from Damian Conway's Perl Best Practices (2005).

           # Name format                 # Salutation
my $salute = $name eq ''                 ? 'Dear Customer'
           : $name =~ /(Mrs?[.]?\s+\S+)/ ? "Dear $1"
           : $name =~ /(.*),\s+Ph[.]?D/  ? "Dear Dr. $1"
           :                               "Dear $name"
           ;
Enter fullscreen mode Exit fullscreen mode

Note that although this is just as inefficient as a cascaded-if/elsif, it’s more clear that it's a single assignment. It’s also more compact and reads like a table with columns of matches and alternatives.

Any of these patterns are preferable to cascading if/elsifs. And if you want to monitor the development of given, when, and ~~, check this issue on GitHub. It was last commented on eight years ago, though, so I wouldn’t hold my breath.

Top comments (5)

Collapse
 
drbaggy profile image
James Smith •

Surely
if(/^abc/) {
last;
}
is more readable and in this case shorter (10 vs 13 symbols) than the horrible

/^abc/ and do {
last;
}

But I do agree that the lookup is better... but sometimes the best is the stacked ternaries if all the brace is doing is assigning...

Collapse
 
mjgardner profile image
Mark Gardner •

Either is fine—I prefer less punctuation, especially in a punctuation-happy language like Perl. And stacked ternaries are great if all you're doing is assigning to the same variable.

Collapse
 
mjgardner profile image
Mark Gardner •

I've amended the post to add an example of stacked ternaries.

Collapse
 
x1madni profile image
x1mandi • • Edited

Hi Mark,

I tried the suggested for loop with topicalizer in one of my scripts. I am developing a reporting tool in Perl (fetch data from DB and generate excel or html reports, then send them via mail).
I am still new to Perl, so I am writing baby Perl - I think that's the term for that :), and I must apologize for the sometimes cumbersome solutions.

Here is how my envrionment looks like:
There is a Collect.pm package,which initializes the "global" scalar $USECASE:

package My::Data::Collect;
use strict;
use warnings;
#Exports
use base 'Exporter';
our @EXPORT_OK = qw($USECASE get_pss_ael);
...
our $USECASE; 
Enter fullscreen mode Exit fullscreen mode

There is report.pl which import My::Data::Collect and assings the value to $USECASE using GetOpt::Long like follows:

GetOptions('type=s' => \$USECASE,
'mode=s' => \$MODE);

and then I start the scirpt: report.pl --type uc1.

Later in the same script I call get_pss_ael() subroutine from My::Data::Collect within a for loop, with $USECASE as topicalizer:

for ($USECASE) {
    /(uc1|uc2|uc3)/ and do {
        get_pss_ael($USECASE);
    }
}
Enter fullscreen mode Exit fullscreen mode

Then out of sudden the $USECASE scalar is undef inside the subroutine call. When I check it with say outside the for loop in the main namespace (report.pl) it is there with the value assigned by GetOpt::Long, in My::Data::Collect, outside any code block and subroutine it is there, although when I pass it to a subroutine it is already undef.
Substituting that for construct with if-else solves that problem.

What's going on there?
Could You please provide further explanation?
I hope I described my issue with enough examples, and good code snippets. If not please let me know and I edit.

Thank You!
Kind Regards,
Csaba

Collapse
 
thibaultduponchelle profile image
Tib •

Great post 👍 thank you a lot