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

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

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