Приглашаем посетить
Татищев (tatischev.lit-info.ru)

Section 6.16.  Value Switches

Previous
Table of Contents
Next

6.16. Value Switches

Use table look-up in preference to cascaded equality tests.

Sometimes an if cascade selects its action by testing the same variable against a fixed number of predefined values. For example:

    sub words_to_num {
        my ($words) = @_;

        # Treat each sequence of non-whitespace as a word...
        my @words = split /\s+/, $words;

        # Translate each word to the appropriate number...
        my $num = $EMPTY_STR;
        for my $word (@words) {
            if ($word =~ m/ zero | zéro /ixms) {
                $num .= '0';
            }
            elsif ($word =~ m/ one | un | une /ixms) {
                $num .= '1';
            }
            elsif ($word =~ m/ two | deux /ixms) {
                $num .= '2';
            }
            elsif ($word =~ m/ three | trois /ixms) {
                $num .= '3';
            }
            # etc. etc. until...
            elsif ($word =~ m/ nine | neuf /ixms) {
                $num .= '9';
            }
            else {
                # Ignore unrecognized words
            }
        }

        return $num;
    }

    # and later...

    print words_to_num('one zero eight neuf');    # prints: 1089

A cleaner and more efficient solution is to use a hash as a look-up table, like so:


    my %num_for = (
    
#   English       Français        Française
'zero' => 0, 'zéro' => 0, 'one' => 1, 'un' => 1, 'une' => 1, 'two' => 2, 'deux' => 2, 'three' => 3, 'trois' => 3,
#        etc.           etc.
'nine' => 9, 'neuf' => 9, ); sub words_to_num { my ($words) = @_;
# Treat each sequence of non-whitespace as a word...
my @words = split /\s+/, $words;
# Translate each word to the appropriate number...
my $num = $EMPTY_STR; for my $word (@words) { my $digit = $num_for{lc $word}; if (defined $digit) { $num .= $digit; } } return $num; }
# and later... print words_to_num('one zero eight neuf');
# prints: 1089

In this second version, words_to_num( ) looks up the lowercase form of each word in the %num_for hash and, if that look-up provides a defined result, appends it to the number being created.

The primary advantage here is that the code in the for loop never need change, no matter how many extra words you subsequently add to the look-up table. For example, if we wished to cater for Hindi digits as well, then you'd need to change the if'd version in 10 separate places:

    for my $word (@words) {
            if ($word =~ m/ zero | zéro | shunya /ixms) {
                $num .= '0';
            }
            elsif ($word =~ m/ one | un | une | ek /ixms) {
                $num .= '1';
            }
            elsif ($word =~ m/ two | deux | do /ixms) {
                $num .= '2';
            }
            elsif ($word =~ m/ three | trois | teen /ixms) {
                $num .= '3';
            }
            # etc.
            elsif ($word =~ m/ nine | neuf | nau /ixms) {
                $num .= '9';
            }

            else {
                # Ignore unrecognized words
            }
        }

But, in the look-up table version, the only change would be to add an extra column to the table itself:


    my %num_for = (
    
#    English       Français         Française          Hindi
'zero' => 0, 'zéro' => 0, 'shunya' => 0, 'one' => 1, 'un' => 1, 'une' => 1, 'ek' => 1, 'two' => 2, 'deux' => 2, 'do' => 2, 'three' => 3, 'trois' => 3, 'teen' => 3,
#        etc.            etc.                             etc.
'nine' => 9, 'neuf' => 9, 'nau' => 9, );

Factoring the translations out into a table also improves the readability of the code, both because the code is more compact, and because tables are a familiar and comprehensible way to structure information.

The values to be looked up in a table don't have to be scalar constants. For example, here's a simple module that installs a debug( ) function, whose behaviour can be configured when the module is loaded:


    package Debugging;

    use Carp;
    use Log::Stdlog  { level => 'debug' };

    
# Choice of actions when debugging...
my %debug_mode = (
# MODE           DEBUGGING ACTION
off => sub {}, logged => sub { return print {*STDLOG} debug => @_; }, loud => sub { carp 'DEBUG: ', @_; }, fatal => sub { croak 'DEBUG: ', @_; }, );
# Change debugging behaviour whenever module is used...
sub import { my $package = shift; my $mode = @_ > 0 ? shift : 'loud';
# Default to carping

        # Locate appropriate behaviour, or die trying...
my $debugger = $debug_mode{$mode}; croak "Unknown debugging mode ('$mode')" if !defined $debugger;
# Install new behaviour...
use Sub::Installer; caller( )->reinstall_sub(debug => $debugger); return; }

The module's import( ) subroutine (which is called whenever the module is loaded) takes a string that specifies how the newly created debug( ) subroutine should behave. For example:


    use Debugging qw( logged );   
# debug(  ) logs messages

That string is unpacked into $mode within the import subroutine, and then used as a look-up key into the %debug_mode hash. The look-up returns an anonymous subroutine that is then installed (via the Sub::Installer module) as the caller's debug( ) subroutine.

Once again, the advantage is that the logic of the import( ) subroutine doesn't have to change when additional debugging alternatives become available. You can simply add the new behaviour (as an anonymous subroutine) to the %debug_mode table. For example, to provide a debugging mode that counts messages:


    

    # Choice of actions when debugging...
my %debug_mode = (
# MODE           DEBUGGING ACTION
off => sub {}, logged => sub { return print {*STDLOG} debug => @_; }, loud => sub { carp 'DEBUG: ', @_; }, fatal => sub { croak 'DEBUG: ', @_; }, counted => do { my $count = 1;
# Private variable for sub
sub { carp "DEBUG: [$count] ", @_; $count++; } }, );

    Previous
    Table of Contents
    Next