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

I've gotten my Case package developed to a point that I think it's about ready for release. I would like some feedback about whether it looks useful, crippled, or whatever. I am including the docs below. Comments about whether they're clear and complete would also be useful.
=head1 NAME Case - lightweight, pure-Perl, multiway decision constructs =head1 VERSION This document describes version 1.00 of Case, released June, 2005. =head1 SYNOPSIS use Case; my $case = dispatcher( 'mozart' => sub { "$_ was a Musician!\n" . Case::action('einstein'); }, qw(dog cat pig) => sub { "$_ is an Animal!\n"; }, qw(einstein newton) => sub { "$_ was a Genius!\n"; }, 'Roy' => sub { "$_ should fall through..." . Case::action(Case::default); }, Case::default => sub { "No idea what $_ is.\n" } ); print $case->('mozart'); or use Case; my $case = dispatcher [ qw(dog cat) ] => sub { print '$_ is an animal'; Case::action(Case::default) }, [ [1,10] ] => sub { print 'number in range'; Case::resume('special') }, special => [ @array, 42 ] => sub { print 'number in list' }, [ qr/\w+/ ] => sub { print 'pattern match' }, [ \%hash ] => sub { print 'entry exists in hash' }, [ \&sub ] => sub { print 'sub returns true value' }, Case::default => sub { print 'default' } ; $case->($val); =head1 DESCRIPTION The Case module provides two ways to determine which of a set of alternatives a given scalar matches, and take the appropriate action. One is more flexible about what types of matches can be done, the other is more efficient at finding an exact string match. =head2 Setting up the test table C<dispatcher> examines the argument list to see which flavor of dispatcher you are creating. For exact string matching (only), the arguments should be strings interspersed with coderefs, as in the first example above. For smart-matching, the arguments should be of the form optional-tag/arrayref-of-tests/coderef. In both cases, the default is preceded by no test (or the equivalent call to C<Case::default>). =head2 Executing a test In either case, the return value is a coderef. Pass a value to that coderef to execute the code associated with (a test that matches) that value. =head2 Modifying the dispatch table The string-matching dispatcher allows you to modify the table by passing more than one argument to the dispatcher you have created. You can define new entries using the same syntax as when originally defining it, and you can delete entries by passing C<undef> as the action coderef associated with the terms you want deleted. Passing a single coderef will redefine the default action. You cannot adjust the smart-matching dispatcher after it has been created. =head2 FALLTHROUGH, CHAINING, and RESUME In C's C<switch> statement, fallthrough exists for you to be able to stack terms up. You get that behavior automatically here by preceding a coderef with all the terms that map to it. Fallthrough from one coderef to the next is discouraged in C, and does not happen in this implementation, per se. Within the action coderefs, you can call the coderef associated with another term or test by calling the magically-defined C<Case::action( +)> with the term (in the case of string-matching flavor), or the tag (in the case of smart-matching flavor) of the test. This is a safer and more flexible way to chain actions together than C-style fallthrough. To call the default case, you can pass no arguments, or (equivalently, but self-documenting), C<Case::default( ) +> as the argument. If you wish to resume testing in the smart-matching flavor, call C<Case::resume(tag)>. Calling resume with no argument will cause it to fall through to the next test (which need not have a tag). B<Note:> The chaining calls are subroutine calls, and will return to the caller just like any subroutine. If the chaining call is not the last statement in your action, you should probably make it part of a C<return> statement. =head2 SMART-MATCHING RULES A match is found when an element of the arrayref, evaluated according to this table, yields a true value: Element ($test) Example Operation ================ ============= ================================== +=== Regex qr/foo|bar/ /$test/ Number 3.14 $_ == $test coderef sub { /foo/ } $test->($_) number range [100,1000] $test->[0] <= $_ and $_ <= $test-> +[1] text range ['a', 'arf'] $test->[0] le $_ and $_ le $test-> +[1] hashref \%hash exists $test->{$_} any other scalar 'a string' $_ eq $test =head2 Ranges An arrayref is interpreted as a range of either numbers or strings. The test will pass if the value being checked is between the two range endpoints (inclusive). The endpoints can appear in either order, but it is an error to have one numeric and one non-numeric, a ref, or not exactly two values in the range specifier. To smart-match against all elements of an array, you can include a ref to the array as a testset (don't wrap it in [] ). To combine it with other tests, make the action C<sub {Case::resume()}> and put the rest of the tests as the next testset. =head2 Coderef Tests Coderef tests are your fallback option. If the test you want to perform is not one of the provided variety, write it yourself and plug it in. Coderefs (both tests and actions) are called with the value being tested aliased to both C<$_> and (the first element of) C<@_>. That is so you can compactly do things like C<sub {s/foo/bar}> and also C<sub {grep /$_[0]/, @foo}>. =head2 Standard Exports The Case module exports C<dispatcher> by default =head2 Optional Exports The following routines will be exported into your namespace if you specifically ask that they be imported: C<default> is a completely empty subroutine, useful as a tag to label the default case (you don't need to import it; C<< Case::default => \&whatever >> looks fine, but you may import it if you want). C<is_number> is a helper function for distinguishing between numbers and strings. =head1 NOTES When smart-matching ordinary scalars, if both are determined to be num +bers, then numeric comparison is done; otherwise, string comparison is done. Stringify or numify as necessary to get the comparison you want. This is not an object-oriented module. Nothing is C<bless>ed into the Case package. =head1 AUTHOR Roy Johnson

Caution: Contents may have been coded under pressure.

Replies are listed 'Best First'.
Re: RFC: Case package documentation
by tlm (Prior) on Jun 02, 2005 at 22:14 UTC
    • One or two targeted examples (e.g. for the "Modifying the dispatch table" section, or in the discussion of fall-through) would be useful; it's a bit dry the way it's written.
    • I am puzzled by the fact that for the smart case the dispatch table cannot be modified after it is created; this is sufficiently different from what I would expect from a dispatch table that it deserves further comment. (I would go as far as to classify it as a bug of sorts, even if it is not fixable.)
    • Does the default case need to be at the end of the case list? I gather that the answer to this is no, but either way it should be explicitly mentioned.
    • I think it is a design mistake to treat the specification of a smart range test as an unordered pair, because it is handy to know that a test of the form [$begin, $end], will be never be true when $begin > $end, which happens often when the endpoints are generated programmatically.
    • I could not make any sense out of this paragraph:
      To smart-match against all elements of an array, you can include a ref to the array as a testset (don't wrap it in [] ). To combine it with other tests, make the action C<sub {Case::resume()}> and put the rest of the tests as the next testset.

    the lowliest monk

      I cannot think of a sensible way to make the smart-match dispatch table modifiable. With the string-table, there's no order of tests, so I can just stick the new test in without regard for where it goes. With the smart-match, there is an order of tests. Do you have suggestions for this?

      I think of modifying the table as a kind of nifty feature that is likely to be little-used. Why do you see it as important?

      The default case does need to be at the end of the list. I will make that explicit in the docs.

      You're probably right about treating ranges as unordered. That's actually a fix where I get to remove code. I had thought it would be a convenience. I also toyed with the idea of a reversed pair signifying "not between". But I don't think there's any value in that, either.

      The perplexing paragraph can probably be removed. It is in anticipation of people considering it a misfeature not to handle arrayrefs as arrayrefs. It merely points out that instead of

      [ $arrayref ] => sub { something }, # if it handled arrayrefs as arra +yrefs
      you could actually do
      $arrayref => sub { something } , # treat $arrayref as a list of te +sts
      and
      # instead of [ $arrayref, 'foo' ] => sub { something }, # do this $arrayref => sub { Case::resume() }, [ 'foo' ] => sub { something },
      I greatly appreciate your comments.

      Caution: Contents may have been coded under pressure.

        I probably overstated my point about the immutability of the smart match dispatch table. My negative reaction upon learning that this table could not be changed stemmed from the idea that one of the strengths of a dispatch table over a hard-coded battery of if-statements (or a switch statement in C, for that matter), is that the dispatch table can be modified during runtime. In fact, such a table can be built by the program from scratch (e.g. upon parsing a configuration file, for example). Therefore it comes as a let down to find something called "dispatcher" (which is strongly suggestive of a dispatch table) that cannot be modified at runtime.

        It really boils down to the nebulous concept of "programmer's expectations". My comments here are just one data point; you probably need a few more :-) .

        How about changing the name of the main function from "dispatcher" to "switch" (as I think you once had it)? In this way you would not be telegraphing an implementation detail that, in this case at least, turns out to be a bit confusing.

        the lowliest monk

Re: RFC: Case package documentation
by Tanktalus (Canon) on Jun 03, 2005 at 04:03 UTC

    I'm trying to figure out ... this isn't re-entrant, is it. Which means, no embedding Case's inside other Cases - even indirectly? Be forewarned: this can make your module extremely limited.

    Of course, Perl does offer some help here - the sub ref you return can localise your stack, and that should help, although it may not be perfect.

      I haven't tested embedding cases*, but I don't see any reason you couldn't. I think there must be something unclear about my documentation to have given you that impression. What made you think that?

      You can download the package from where I've stashed it if you want to play with it. There's no installation stuff (Makefile.PL), you just unpack it and work within the Case directory. There is a small test suite (t.pl, runs tests in the t subdirectory) that should illustrate how I designed it to be used. (Of course, my goal is to have the docs explain it without the need for more clarification.)

      The interface is definitely different from Switch, which is the reason I named the function dispatcher: the function gives you a dispatcher, rather than acting like a piece of flow-control syntax.

      * Ok, I just added a simple test for embedded cases, and it works. It's not in the stashed version. To the end of smatch.t, I've added

      # $smatch was created by a previous call to dispatcher my $embed = dispatcher( ['ok'] => sub { $smatch->($_) } ); is($embed->('ok'), 'resumed and matched', 'embedded smatch');

      Caution: Contents may have been coded under pressure.