Beefy Boxes and Bandwidth Generously Provided by pair Networks
Problems? Is your data what you think it is?
 
PerlMonks  

Simple Switch statement

by knexus (Hermit)
on Sep 12, 2003 at 19:03 UTC ( [id://291159]=perlquestion: print w/replies, xml ) Need Help??

knexus has asked for the wisdom of the Perl Monks concerning the following question:

All,

I would like to use a simple Switch/Case setup for processing discontinuous numerical values and/or string error codes. I have read through the posts/faq that I could find on perl monks as well as the Documentation on the CPAN Switch module. If after reading this you feel I missed a post or something, please let me know.

To me, the Switch module seems like overkill for what I want to accomplish. Plus the comment in the module doc itself "There are undoubtedly serious bugs lurking somewhere in code this funky :-)" gives me pause. While I appreciate the candor, it makes me not want to use it for something simple unless there's no other choice. You know "Keep It Simple Stupid!"

Anyhow, after reviewing what I could find, it seemed to me that there must be some way to do it like a jump table.
Now, I know goto is usually shunned by programmers (myself included) but as distastefull as it can be at times, it seems to be useful in this case. The code below handles known/unknown cases. If the programmer can guarantee only valid cases there's a more simple "Switch" that is commented out in the example.

So, basically what I am asking for is feedback/constructive criticism of the approach shown below.
Being relatively new to perl I am wondering if I am overlooking something important. This seems too simple of an approach to not be mentioned somewhere... UNLESS it's flawed :-)

Note: I just made a few test cases for my own sanity checking, there's nothing special about them.

Thanks for any help/comments.

#!/usr/bin/perl -w use strict; sub HandleSomething { my $option = shift; # SWITCH:{ goto 'CASE_'.$option; # Use if always good cases SWITCH:{$_='CASE_'.$option; eval("goto $_"); $_= 'DEFAULT' if ($@) +; goto $_; CASE_1: print "Case 1\n"; last SWITCH; CASE_4: print "Case 4: "; for (1..4) { print $_; } print "\n"; last SWITCH; CASE_FOUR: print "Case FOUR\n"; last SWITCH; CASE_9: print "Case 9\n"; return "Because I can"; CASE_WHAT: ; CASE_STR: print "Case STRING on $option\n"; last SWITCH; DEFAULT: print qq|Undefined Case "$option"\n|; } } my @testCases = ( 1, 4, 'FOUR', '9','WHAT',"STR", 2, 'nine'); my $more = { tst => 4, tst4 => 'FOUR', tst5 => "Opps" }; print '-' x 50 . "\n"; for (@testCases) { HandleSomething $_; } print '-' x 50 . "\n"; for (sort values %{$more}) { HandleSomething $_; }

Replies are listed 'Best First'.
Re: Simple Switch statement
by tantarbobus (Hermit) on Sep 12, 2003 at 19:15 UTC

    When I need a switch I normally do something like this:

    my %switch = ( default => \&list_ads, submit_payment => \&save_payment, set_duration => \&set_duration, go_here => sub {print 'hi'}, ); my $value = "go_here"; if (defined($value) && exists($switch{$value})) { $switch{$value}->(); } else { $pages{'default'}->(); }
      FWIW, I usually write this:
      if (defined($value) && exists($switch{$value})) { $switch{$value}->(); } else { $switch{'default'}->(); # I assume $pages was a mistake }
      as:
      ($switch{$value} || $switch{'default'})->();

      Liz

Re: Simple Switch statement
by dragonchild (Archbishop) on Sep 12, 2003 at 20:21 UTC
    A switch statement is basically a dispatch table. I would use something like that. Other options would include something like:
    for ($val) { /^1$/ && do { do_something }; /^abc$/ && do { do_something_else }; /^\d{2,4}$/ && do { do_something_further }; }

    This is straight out of the Camel.

    ------
    We are the carpenters and bricklayers of the Information Age.

    The idea is a little like C++ templates, except not quite so brain-meltingly complicated. -- TheDamian, Exegesis 6

    Please remember that I'm crufty and crochety. All opinions are purely mine and all code is untested, unless otherwise specified.

      I normally do just as dragonchild suggests, which is indeed right out of holy writ. However, don't be too quick to give up the old if/then/else. While maybe not as clean looking or as cool, the venerable standby can be about 2x as fast, though, admittedly, usually not noticeable unless you are testing thousands of conditions :-)

      Switch/case is one of the RARE features I miss from traditionally frowned upon javascript.
        I agree with what bradcathey said about if/then/else. Also on testing of simple strings/values. The mundane
        if ($a eq "simple value") { ... }
        runs faster than
        if ($a =~ /^simple value$/) { ... }
        The point is to avoid using regular expressions on simple string testing. Any bit of speed increase in perl is good, especially when it has to be evaluated countless times.

Re: Simple Switch statement
by hossman (Prior) on Sep 12, 2003 at 19:45 UTC
    Plus the comment in the module doc itself "There are undoubtedly serious bugs lurking somewhere in code this funky :-)" gives me pause.

    That comment is most likely just in regard to some of the more complicated things it tries to support (like switching on an object ref, and a method name, or matching a hash ref against a regexp)

    If you are mainly interested in basic equality matching of values (which is what it looks like from your example) then I think you're relatively safe.

    The best way to decide if a module is "bug free" enough for you, is to see what the CPAN Testers had to say, then look aver the test suites used and decide if they cover the usage you are interested in. If so, you're probably okay ... but if you're not sure, then write your own set of tests, and if they fail, then submit them to the author as a bug report.

    Speaking of bug reports, there are a few open bugs for Switch which you might want to look at to see what kinds of bugs people have allready found

Re: Simple Switch statement
by BrowserUk (Patriarch) on Sep 13, 2003 at 07:22 UTC

    I like this enough that I will be using it for simple case statements.

    You can simplify the syntax of it somewhat and make it read almost like the C version by declaring a sub called switch. It's actually more flexible being able to use (some) strings as well as integers.

    Updated: Added check for none label characters in the switch expression in view of Courages concerns, allthough in this version, they just caused the default action without the check, the error reporting is handy.

    #! perl -slw use strict; sub switch{ die "Bad switch expression at @{[ caller() ]}\n" unless $_[0] =~ /^\w+$/; eval{ goto "case_$_[0]" } or goto default; } for my $expr ( 1 .. 10, 'fred', "1;system('rm -rf /')" ) { switch( $expr ); { case_1: print '1'; last; case_2: print '2'; last; case_3: print '3'; last; case_4: print '4'; last; case_5: ; case_6: print '5 or 6'; last; case_fred: print 'fred'; last; default: print "default"; } } __END__ P:\test>test 1 2 3 4 5 or 6 5 or 6 default default default default fred Bad switch expression at main P:\test\test.pl8 10

    Two things to watch for. The semi-colon after the switch(expr); and the need for a semi-colon after a label without a body (as in case_5: ; above).

    I'm actually not quite sure why this latter one is required, but it seems to be. Unless someone can tell me where it is written that you can't have two consequetive labels in perl code, I'd consider this a bug?


    Examine what is said, not who speaks.
    "Efficiency is intelligent laziness." -David Dunham
    "When I'm working on a problem, I never think about beauty. I think only how to solve the problem. But when I have finished, if the solution is not beautiful, I know it is wrong." -Richard Buckminster Fuller
    If I understand your problem, I can solve it! Of course, the same can be said for you.

      For scoping reasons, I would change:
      sub switch{ eval{ goto "case_$_[0]" } or goto default; } for my $expr ( 1 .. 10, 'fred' ) { switch( $expr ); {
      to:
      my $switch = sub { eval{ goto "case_$_[0]" } or goto default; }; for my $expr ( 1 .. 10, 'fred' ) { $switch->( $expr ); {
      but other than that, I must say I like this idiom as well. If you're not interested in performance, that is. Because the performance of the switch like this, is abysmal compared to an identical if/elsif/else structure, as this little benchmark shows:
      use strict; use Benchmark; timethese( 50000,{ switch => sub { sub switch{ eval{ goto "case_$_[0]" } or goto default; } for my $expr ( 1 .. 10, 'fred' ) { switch( $expr ); { case_1: print STDERR '1'; last; case_2: print STDERR '2'; last; case_3: print STDERR '3'; last; case_4: print STDERR '4'; last; case_5: ; case_6: print STDERR '5 or 6'; last; case_fred: print STDERR 'fred'; last; default: print STDERR "default"; } } }, if => sub { for my $expr ( 1 .. 10, 'fred' ) { if ($expr eq '1') {print STDERR '1'} elsif ($expr eq '2') {print STDERR '2'} elsif ($expr eq '3') {print STDERR '3'} elsif ($expr eq '4') {print STDERR '4'} elsif ($expr eq '5' or $expr eq '6') {print STDERR '5 or 6'} elsif ($expr eq 'fred') {print STDERR 'fred'} else {print STDERR "default"} } }, } );
      Please note that I changed the print statements to "print STDERR" so that I could send them to the bitbucket. The result of the benchmark:
      Benchmark: timing 50000 iterations of if, switch if: 7 wallclock secs ( 5.41 usr + 0.00 sys = 5.41 CPU) @ 92 +42.14/s (n=50000) switch: 43 wallclock secs (39.85 usr + 0.00 sys = 39.85 CPU) @ 12 +54.71/s (n=50000)

      So the switch structure is at least 7 times as a comparable id/elsif/else structure. So don't put this in very deep and tight loops!

      Liz

      Edit by tye, change PRE to CODE around long lines

        Performance or lack of it in the switch:

        This really caught me by surprise, as to me it just seems counter intuitive that indexed jumps would be slower, especially by such a huge margin, but hey this is perl not 'C', what do I know? ;-)

        So, I took the code liz provided in post, which was so kindly benchmarked. I removed the prints to reduce dilution effect of typically long operations like print and replaced them with just an assignment; bart addressed this as well here. The following is a summay of things I looked at and the results:

      Thanks for tweeking the code, I like how this looks over my original post. I was going for the 'C'ish look and this is much closer.

      However,I am puzzled/concerned over the technique of calling into  sub switch() and jumping out of it never to return. I have no idea if this is valid/acceptable in perl or not. Although it certainly appears to be OK in this example.

      In other languages I have used, this is a technique I use only very carefully... being sure to manipulate the stack/etc accordingly to avoid stack problems down the road, like overflow and/or returning to oblivion at some point in the future.

      Would/Could this technique bring out nasty side-effects... perhaps in larger applications or those running over extened periods of time?
      Is this of any concern in Perl scripts?

        Perhaps a quote from perlsyn is the best answer.

        Although not for the faint of heart, Perl does support a goto statement. There are three forms: goto-LABEL, goto-EXPR, and goto-&NAME. A loop's LABEL is not actually a valid target for a goto; it's just the name of the loop.

        The goto-LABEL form finds the statement labeled with LABEL and resumes execution there. It may not be used to go into any construct that requires initialization, such as a subroutine or a foreach loop. It also can't be used to go into a construct that is optimized away. It can be used to go almost anywhere else within the dynamic scope, including out of subroutines, but it's usually better to use some other construct such as last or die. The author of Perl has never felt the need to use this form of goto (in Perl, that is--C is another matter).

        I beleive that makes it safe to use this way, though LW's comment is worth reading twice. In this case, I think that the result is as 'structured' as many of the alternatives and much cleaner than most of them.

        It is also at least as efficient as the Switch module which itself uses goto amongst several other fairly esoteric practices, though obviously far less flexible.

        As for the performance, goto is known to be slow, but the benchmarks I've seen offered in this thread so far don't take into account the fact that using if..elsif...else cascades require multiple conditions to be evaluated for those cases near the end of the cascade. Of course this can be mitigated somewhat by careful ordering of the elements if the frequencey distribution of the conditions is predictable, but even then, if all paths are exercised the same number of times, there is no optimal ordering. Using goto bypasses this dilema. Whether this is ever enough to offset the slowness of goto will depend on the application. Used at a high level in the code, with non-trivial conditional code blocks this probably isn't much of a concern. I would definitely favour the hash-based dispatch method for performance critical application though.


        Examine what is said, not who speaks.
        "Efficiency is intelligent laziness." -David Dunham
        "When I'm working on a problem, I never think about beauty. I think only how to solve the problem. But when I have finished, if the solution is not beautiful, I know it is wrong." -Richard Buckminster Fuller
        If I understand your problem, I can solve it! Of course, the same can be said for you.

Re: Simple Switch statement
by Courage (Parson) on Sep 13, 2003 at 07:04 UTC
    Your idea is beautiful, and other responces showed beautiful ideas also.

    But DO NOT use eval "goto $_": your code will be vulnerable to hacker crackery: imagine someone will provide to you value of "1;system('rm -rf /')". (I can imagine such a data somewhere in DB and you process it...)

    If you do not change this, terrible things could happen.

    Courage, the Cowardly Dog

      Your particular example won't work.

      #!/usr/bin/perl -wl $_ = q~BOB;print "not good"~; eval "goto $_"; exit; print "hello"; __END__ hello

      However, there is a risk if they use &&. Just being picky. :)

        Ok, consider this:
        1; BEGIN{ system('echo MUAHAHA!!') }
        indeed.

        But hackery crackery was left as an excercise for a reader

        :):):")

Re: Simple Switch statement
by mattr (Curate) on Sep 13, 2003 at 07:20 UTC
    I sympathize, but mentioned 7 months ago in Re: Complex dispatch table that I had met with extreme problems using the standalone Switch module so don't use it. It seems great, but people said it was my fault for using something which was not meant to be of production quality. It seemed to trigger instabilities which showed up only when parsing the program sometimes, for example behavior changed with whitespace or by moving lines around in the program. I have not used Switch for case statements since then and usually use a hash for dispatch tables, so you have been warned. If you are using something built into perl itself now which I have missed then perhaps it is safe..
Re: Not-so-Simple Switch statement
by NetWallah (Canon) on Sep 13, 2003 at 21:46 UTC
    I embellished liz's benchmark, adding another implementation of the "switch" equivalent, and ran some tests.

    I use a hash to store subroutine references, and index into the hash. This requires the same amount of code as the other methods, but I feel it is more "Perly". I think it is also more scalable.

      When doing such benchmarks, one should remember that printing is a very slow process. Even more, the time it takes is likely not very constant. So in order to get a real feel for how long the code-under-test really takes, you should do something fast and harmless, like setting a variable. So I replaced everything looking like
      print STERR 'foo';
      with
      $out = 'foo';

      I do get quite different results than yours. I always get the faster result for "HashSub" over "if", while with you, it's slower. Odd.

        I re-ran the tests too, replacing the "print STDERR" with an assignment.

        The "if" and "HashSub" methods speeds converge faster, with Hashsub becoming faster than "if" at a 22% match rate.

        The minor differences in results may have to do with OS and perl implementations. I'm running Activestate perl 5.8 under Windows XP.

Log In?
Username:
Password:

What's my password?
Create A New User
Domain Nodelet?
Node Status?
node history
Node Type: perlquestion [id://291159]
Approved by gmax
Front-paged by Courage
help
Chatterbox?
and the web crawler heard nothing...

How do I use this?Last hourOther CB clients
Other Users?
Others taking refuge in the Monastery: (6)
As of 2024-09-16 21:39 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?
    The PerlMonks site front end has:





    Results (22 votes). Check out past polls.

    Notices?
    erzuuli‥ 🛈The London Perl and Raku Workshop takes place on 26th Oct 2024. If your company depends on Perl, please consider sponsoring and/or attending.