Beefy Boxes and Bandwidth Generously Provided by pair Networks
Welcome to the Monastery
 
PerlMonks  

Switch/case as a dispatch table with C-style fall-through

by Roy Johnson (Monsignor)
on May 11, 2005 at 19:41 UTC ( #456132=snippet: print w/ replies, xml ) Need Help??

Description: I plan to make a collection of various flavors of case/switch/given and package them up for CPAN as one package, probably named "Case.pm". This one is very C-flavored, with a twist. It constructs a hash associating your options with their actions, and returns a closure (necessary for handling default and fall-through). Then, when you need to execute the case, you just pass your term to the closure, and the appropriate sub(s) are executed.

I even wrangled a break() statement. No default identifier, though.

Comments on flaws or possible enhancements* would be welcome.

* within the limitations of using a hash jump table


Revision 1:
  • added a no-op subroutine to act as the default flag, and also as the default-default case
  • the term is passed to the subroutines
  • croaks on malformed input
  • simplified code by processing input list in reverse
Revision 2:(posted as followup, below)
  • Got rid of break(); added local $Case::action() for chaining subs
  • Die on non-CODE refs; warn on unreachable action block
package Swash;

# This is a tag you can stick in the list for clarity
# also serves as default routine when none is specified
sub default {}

# Make a key for each non-ref entry; the value is a coderef
# that executes the first subsequent coderef in the list
# Plus some bookkeeping to make fallthrough happen
sub new {
    my %swash;
    my ($default, $nextcode, $gotcode) = (\&default) x 2;
    my $call_pkg = caller;
    for my $item (reverse @_) {
        if (ref $item eq 'CODE') {
            if ($gotcode) {
                if (keys %swash) {
                    use Carp;
                    croak "Malformed swash: non-default coderef with n
+o associated term found";}
                $nextcode = $default = $gotcode;
            }
            $gotcode = $item;
        }
        elsif ($gotcode) {
            my ($case_code, $fallthru_code) = ($gotcode, $nextcode);
            $swash{$item} = sub {
                    my $fallthru = 1;
                    no strict 'refs';
                    no warnings 'redefine';
                    local *{$call_pkg.'::break'} = sub { $fallthru = 0
+ };
                    $case_code->($_[0]);
                    $fallthru_code->($_[0]) if $fallthru;
                };
            $nextcode = $swash{$item};
            ($gotcode, @keys) = ();
        }
        else {
            $swash{$item} = $nextcode;
        }
    }
    return sub { my ($term) = @_; ($swash{$term} || $default)->($term)
+ };
}

1;

package main;

my $case = Swash::new(
   qw(mozart)          => sub { print "$_[0] was a Musician!\n" },
   qw(einstein newton) => sub { print "$_[0] was a Genius!\n"; break()
+; },
   qw(dog cat pig)     => sub { print "$_[0] is an Animal!\n"; break()
+; },
   'Roy'               => sub { print "$_[0] should fall through..." }
+,
   Swash::default      => sub { print "No idea what $_[0] is.\n" }
);   

for (qw(mozart cat PerlMonk newton pig einstein)) {
    print "Looking up $_...\n";
    $case->($_);
}
print "And Roy?\n";
$case->('Roy');
Comment on Switch/case as a dispatch table with C-style fall-through
Download Code
Re: Switch/case as a jump table with C-style fall-through
by Animator (Hermit) on May 12, 2005 at 13:00 UTC

    Some of my thoughts:

    Not sure if it is useful, but how about calling the coderef with the value it triggerd? (as in, $case->("cat"); runs the coderef of the animal with the argument 'cat')

    Here is a suggestion of which I'm really not sure: maybe it would be better to use a different name then 'new'? Pepole might expect a blessed reference and not a coderef... when you use new someone might think he should do something like $case->do_case(), so be really careful with how you name it...

    And an identifier for the default code/value might be useful aswell, and/or maybe a continue/finalise block? (I did see some discussion about finalise before (but that was in another language), I leave it up to you to decide wheter or not that is useful)

    A suggestion I was planning to make when I first looked at it was to allow a scalar instead a coderef too, but after taking a better look I realised this was not possible, since multiple keys can refer to the same code. (unless when there is a static-element added before the start of every key-block... which would mean you can just look at the one matched before, but this will make it more complex I guess)

    update: typo :(

      I also wasn't sure it would be useful, but in the case of fall-through, I can see how it might come in handy to pass the matched string. That will go into the next revision.

      I'm not married to the name 'new' (and when I package it with other flavors of Case, it will certainly have a different name), but I didn't have a better name. I think it's just as legitimate to call a factory "new" as it is to call an OO constructor "new". The users are expected to read the documentation.

      I left out a default identifier because I didn't want to have a string that might conflict with an alternative. But it occurred to me that a call to a subroutine that returns an empty list could be tucked in there and be absolutely transparent. That will also be in the next revision.

      I don't know what a continue/finalise block should do here. Could you explain?

      Thanks for your input.


      Caution: Contents may have been coded under pressure.

        I see now that I basiclly took it from the wrong concept... (what I initially though about had more to do with something like eval then with switch)

        I have no idea wheter or not it would be a good idea to implement what I was thinking about... I'll continue explaining and then you can make up your mind... I was thinking about creating a 'handler' that is run at the end if one (or more) case-statements matched...

        I'm afraid I didn't explain to well and that it might be easier to explain it with an example/code, so that's what I will do now:

        Assume that at the end of each case there is some default action that should be taken (update a hash with a list of matches or something). With the current implementation there are two ways to accomplish this:

        a) adding the code it in each code-ref (by calling another subroutine)
        b) using something like $case->("something") and some_other_code; (although in this case it would depend on the return value, but let's assume it returns a true value when the code-ref was run)

        Both of these have some disadvantages, the first one has the disadvantage of the duplicate function-call and the second depends on the return value

        What my suggestion was about was to be able to set up something like 'FINAL' (or 'CONTINUE') => sub { ... } which will run the code (add it to a hash or whatever) if a case returned true

Re: Switch/case as a jump table with C-style fall-through
by dragonchild (Archbishop) on May 12, 2005 at 13:18 UTC
    Basically, you're trying to come up with an API for a generalized switch statement without using source filters in Perl5. The actual mechanics, frankly, are irrelevant.

    While I don't have much in the way of implementation suggestions, I do have a few requirements that I'd like to see from a switch statement. You may already implement some of these - I haven't checked.

    • Ordering of cases. This is very important with fall-through, because I may only want fall-through to a specific location. So, something like:
      switch (i) { 0,1: foo(); 2: bar(); break; 3: baz(); break; };
      0 and 1 must fall through to 2, but not 3. It also matters with the case of regex matches (q.v. below).
    • Smart matching. If I give you '5' as a case, I want '05' to match it. If I give you qr/^boo.*boo$/, I want you to match 'booboo' and 'booBOOboo'. If I give you a code reference, I want you to pass the value to the coderef which will return true or false. And, so forth.
    • Options for how matching happens. I might want a switch statement to be case-sensitive or case-insensitive. I might want matches to always be string-matches (in which case '5' and '05' are different) or I might want them to be smart.
    • I want a redo statement in my switch. This is actually very useful in parsing, particularly something like CSV data. Text::xSV basically implements a switch statement with redo as its parsing engine. (Oh, and the cases are primarily regexes.)

    Taking a page from Class::MakeMethods, it might be useful to have each case be an arrayref. The last value is the action to take. All others are the case options. If the first value begins with '--', then it's an optional modifier to how to handle the next case option. So, maybe something like:

    my $switch = Switch->new( [ '5', sub {} ], # Matches '005' [ '--string-match', '6', sub {} ], # Doesn't match '006', [ qr/floober/, sub {} ] # regex match [ sub { $foo->bar( @_ ) }, sub {} ] # coderef [ 1 .. 3, 8 .. 20, sub {} ] # Multiple cases );

    That would be a useful module.


    • In general, if you think something isn't in Perl, try it out, because it usually is. :-)
    • "What is the sound of Perl? Is it not the sound of a wall that people have stopped banging their heads against?"
      Actually, the mechanics are important. My objective in this was to address the complaints that switch is just a chain of if-elses: that all the alternatives have to be searched until a match is found. I have implemented it as a dispatch table to eliminate searching. That precludes "smart matching", though. Only strict equivalence can be done this way (ok, anything's possible, but it's not reasonable).

      For more of what you're looking for, I recommend my previous posting, (Revisiting) Smart match in p5. It looks a lot like your example, though I didn't turn it into an object, and I'm not sure there's any advantage to doing so. It will be another flavor to be included in the Case package.


      Caution: Contents may have been coded under pressure.
Re: Switch/case as a dispatch table with C-style fall-through
by Animator (Hermit) on May 13, 2005 at 10:00 UTC

    Here are some others thoughts:

    In your current implementation you define the break function in the caller's package, but what if there is already a break function in it?

    Wouldn't it be better to allow a user-defined name, and/or return a blessed version of the coderef and make it possible to call the break method on it? (in order to this to work properly you will need to pass the object itself to the code-ref of the eval)

    And shouldn't the break function stop the execution of the code? (although this might be rather complicated..., as in adding 'last' to the break sub won't work if the code has it's loop, using die/eval might work but that would be slower...)

    And what about implementing next, redo and last?

    Where next could mean, jump to the next case statement (that is the fallthru one, possibile with or without ignoring the fallthru variable (if break doesn't return immediatly))

    Redo could mean redo the case-coderef. (Updated (old text: entire switch statement, this might be useful when a certain case code-ref modifies the input and wants to re-check it...)

    And last could be an alias to break...

    By creating your own loop you can figure out which one was called... Some code to demonstrate:

    my ($redo, $next, $last)=(0, 0, 1); { my $first = 0; for (0, 1) { if (not $_ and $first++) { $redo=1; $last = 0; last; } if ($_) { $next = 1; $last = 0; last; } main_code(); $last = 0; last; } } $,=" & "; print ($next, $redo, $last); sub main_code { redo; } # replace redo with next and/or last to test

      Good things to think about.

      While I was proud of how I got a break() into the mix, it smells more like a trick than a well-designed component of the tool. As you point out, it should really stop execution of the current sub as well as preventing execution of the fallthrough sub, and I don't think I can make it do that.

      If the user had defined a break() sub of his own, it would be unavailable in the switch subs, masked by the one I defined. But outside of that, it would be perfectly functional. If the user wanted to use it in the switch, he'd have to take a reference to it and call that. I thought it was a reasonable compromise, but I think I have a better, break()-less design in mind, now.

      In the K&R book on C, the double-edged sword of fallthrough is addressed. Programmers are encouraged to use it only to apply one code block to multiple conditions, rather than to have one code block fall through to another. That is how I plan to make my case statement operate. Suddenly, there's no more need for break().

      For the exceptional cases where chaining is desired, I will provide a local $Case::this (or $Case::self, whatever...maybe $Case::chain) variable that the user can use for recursing or chaining cases. So instead of introducing new pseudo-keywords (and the compromises they bring), we get less-cluttered, DWIM normal operation, and a flexible facility for doing tricky things if needed. The features you requested would translate thus:
      last return
      next $Case::chain->('specified term'); return
      redo goto &$Case::chain
      I particularly like having an explicit target for fall-through, which specifically addresses the dangers of C-style fall-through. Oh, and I use $_ instead of $_[0] because that's where I think the current term should go.

      I don't think I've designed anything impossible. I won't be able to implement it until next week, though.


      Caution: Contents may have been coded under pressure.
Revision 2: Switch/case as a dispatch table with C-style fall-through
by Roy Johnson (Monsignor) on May 16, 2005 at 15:20 UTC
    I have reworked the construct as discussed earlier. I include a minimal test suite here to demonstrate the expected behavior. and the module itself, which is more lightweight and (I think) elegant than before:

    Caution: Contents may have been coded under pressure.

Back to Snippets Section

Log In?
Username:
Password:

What's my password?
Create A New User
Node Status?
node history
Node Type: snippet [id://456132]
help
Chatterbox?
and the web crawler heard nothing...

How do I use this? | Other CB clients
Other Users?
Others avoiding work at the Monastery: (9)
As of 2014-12-23 04:44 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?

    Is guessing a good strategy for surviving in the IT business?





    Results (135 votes), past polls