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

NAME

Dispatch::Fu - Converts any complicated conditional dispatch situation into familiar static hash-key based dispatch

SYNOPSIS

  use strict;
  use warnings;
  use Dispatch::Fu; # 'dispatch', 'cases', 'xdefault', and 'on' are exported by default, just for show here

  my $INPUT = [qw/1 2 3 4 5/];

  my $results = dispatch {                                      # <~ start of 'dispatch' construct
      my $input_ref = shift;                                    # <~ input reference
      return ( scalar @$input_ref > 5 )                         # <~ return a string that must be
       ? q{case5}                                               # <~ defined below using the 'on'
       : sprintf qq{case%d}, scalar @$input_ref;                # <~ dynamic dispatch here
  } $INPUT,                                                     # <~ input reference, SCALAR passed to dispatch BLOCK
    on case0 => sub { my $INPUT = shift; return qq{case 0}},    # <~ if dispatch returns 'case0', run this CODE
    on case1 => sub { my $INPUT = shift; return qq{case 1}},    # <~ if dispatch returns 'case1', run this CODE
    on case2 => sub { my $INPUT = shift; return qq{case 2}},    #    ...   ...   ...   ...   ...   ...   ...
    on case3 => sub { my $INPUT = shift; return qq{case 3}},    # ...   ...   ...   ...   ...   ...   ...   ...
    on case4 => sub { my $INPUT = shift; return qq{case 4}},    #    ...   ...   ...   ...   ...   ...   ...
    on case5 => sub { my $INPUT = shift; return qq{case 5}};    # <~ if dispatch returns 'case5', run this CODE

DESCRIPTION

Dispatch::Fu provides an idiomatic and succinct way to organize a HASH-based dispatch table by first computing a static key using a developer defined process. This static key is then used immediate to execute the subroutine reference registered to the key.

This module presents a generic structure that can be used to implement all of the past attemts to bring things to Perl like, switch or case statements, given/when, smartmatch, etc.

The Problem

HASH based dispatching in Perl is a very fast and well established way to organize your code. A dispatch table can be fashioned easily when the dispatch may occur on a single variable that may be one or more static strings suitable to serve also as HASH a key.

For example, the following is more or less a classical example of this approach that is fundamentally based on a 1:1 mapping of a value of $action to a HASH key defined in $dispatch:

  my $CASE = get_case(); # presumed to return one of the hash keys used below

  my $dispatch = {
    do_dis  => sub { ... },
    do_dat  => sub { ... },
    do_deez => sub { ... },
    do_doze => sub { ... },
  };

  if (not $CASE or not exists $dispatch->{$CASE}) {
    die qq{case not supported\n};
  }

  my $results = $dispatch->{$CASE}->();

But this nice situation breaks down if $CASE is a value that is not suitable for us as a HASH key, is a range of values, or a single variable (e.g., $CASE) is not sufficient to determine what case to dispatch. Dispatch::Fu solves this problem by providing a stage where a static key might be computed or classified.

The Solution

Dispatch::Fu solves the problem by providing a Perlish and idiomatic hook for computing a static key from an arbitrarily defined algorithm written by the developer using this module.

The dispatch keyword and associated lexical block (that should be treated as the body of a subroutine that receives exactly one parameter), determines what case defined by the on keyword is immediately executed.

The simple case above can be trivially replicated below using Dispatch::Fu, as follows:

  my $results = dispatch {
    my $case = shift;
    return $case;
  },
  $CASE,
   on do_dis  => sub { ... },
   on do_dat  => sub { ... },
   on do_deez => sub { ... },
   on do_doze => sub { ... };

The one difference here is, if $case is defined but not accounted for using the on keyword, then dispatch will throw an exception via die. Certainly any logic meant to deal with the value (or lack thereof) of $CASE should be handled in the dispatch BLOCK.

An example of a more complicated scenario for generating the static key might be defined, follows:

  my $results = dispatch {
    my $input_ref = shift;
    my $rand  = $input_ref->[0];
    if ( $rand < 2.5 ) {
        return q{do_dis};
    }
    elsif ( $rand >= 2.5 and $rand < 5.0 ) {
        return q{do_dat};
    }
    elsif ( $rand >= 5.0 and $rand < 7.5 ) {
        return q{do_deez};
    }
    elsif ( $rand >= 7.5 ) {
        return q{do_doze};
    }
  },
  [ rand 10 ],
   on do_dis  => sub { ... },
   on do_dat  => sub { ... },
   on do_deez => sub { ... },
   on do_doze => sub { ... };

The approach facilited by Dispatch::Fu is one that requires the programmer to define each case by a static key via on, and define a custom algorithm for picking which case (by way of return'ing the correct static key as a string) to execute using the dispatch BLOCK.

USAGE

For more working examples, look at the tests in the ./t directory. It should quickly become apparent how to use this method and what it's for by trying it out. But if in doubt, please inquire here, there, everywhere.

dispatch BLOCK

BLOCK is required, and is coerced to be an anonymous subroutine that is passed a single scalar reference; this reference can be a single value or point to anything a Perl scalar reference can point to. It's the single point of entry for input.

  my $results = dispatch {
    my $input_ref = shift; # <~ there is only one parameter, but can a reference to anything
    my $key = q{default};  # <~ initiate the default key to use, 'default' by convention not required
    ...                    # <~ compute $key (yada yada)
    return $key;           # <~ key must be limited to the set of keys added with C<on>
  }
  ...

The dispatch implementation must return a static string, and that string should be one of the keys added using the on keyword. Otherwise, an exception will be thrown via die.

Note: dispatch faithfully returns whatever the dispatched subroutine is written to return; including a single value SCALAR, SCALAR refernce, LIST, etc.

  my @results = dispatch {
    my $input_ref = shift; # <~ there is only one parameter, but can a reference to anything
    my $key = q{default};  # <~ initiate the default key to use, 'default' by convention not required
    ...                    # <~ compute $key (yada yada)
    return $key;           # <~ key must be limited to the set of keys added with C<on>
  }
  ...
  on default => sub { return qw/1 2 3 4 5 6 7 8 9 10/ },
  ...

If the default case is the one dispatched, then @results will contain the digits 1 .. 10 returned as a LIST via qw//, above.

cases

This routine is for introspection inside of the dispatch BLOCK. It returns the list of all cases added by the on routine. Outside of the dispatch BLOCK, it returns an empty HASH reference. It is available within the CODE body of each case handler registered via `on`.

Note: do not rely on the ordering of these cases to be consistent; it relies on the keys keyword, which operates on HASHes and key order is therefore not deterministic.

Given the full example above,

  my $results = dispatch {
    my $input_ref = shift;
    ...
    my @cases = cases; # (qw/do_dis do_dat do_deez do_doze/)
    ...
  },
  [ rand 10 ],
   on do_dis  => sub { ... },
   on do_dat  => sub { ... },
   on do_deez => sub { ... },
   on do_doze => sub { ... };
xdefault SCALAR, [DEFAULT_STRING]

Note: SCALAR must be an actual value (string, e.g.) or undef.

Provides a shortcut for the common situation where one static value really define the case key. Used idiomatically without the explicit return provided it is as the very last line of the dispatch BLOCK.

  my $results = dispatch {
    my $input_str = shift;
    xdefault $input_str, q{do_default}; # if $input_str is not in supported cases, return the string 'default'
  },
  $somestring,
   on do_default => sub { ... },        #<~ default case
   on do_dis     => sub { ... },
   on do_dat     => sub { ... },
   on do_deez    => sub { ... },
   on do_doze    => sub { ... };

xdefault can be passed just the string that is checked for membership in cases, if just provided the default case key, the string default will be used if the string being tested is not in the set of cases defined using on.

  my $results = dispatch {
    my $input_str = shift;
    xdefault $input_str;      # if $input_str is not in the set of supported cases, it will return the string 'default'
  },
  $somestring,
   on default => sub { ... }, #<~ default case
   on do_dis  => sub { ... },
   on do_dat  => sub { ... },
   on do_deez => sub { ... },
   on do_doze => sub { ... };

And just for the sake of minimization, we can get rid of one more line here:

  my $results = dispatch {
    xdefault shift;           # if $input_str is not in supported cases, return the string 'default'
  },
  $somestring,
   on default => sub { ... }, #<~ default case
   on do_dis  => sub { ... },
   on do_dat  => sub { ... },
   on do_deez => sub { ... },
   on do_doze => sub { ... };
REF

This is the singular scalar reference that contains all the stuff to be used in the dispatch BLOCK. In the example above it is, [rand 10]. It is the way to pass arbitrary data into dispatch. E.g.,

  my $INPUT  = [qw/foo bar baz 1 3 4 5/];

  my $result = dispatch {
    my $input_ref = shift; # <~ there is only one parameter, but can a reference to anything
    my $key = q{default};  # <~ initiate the default key to use, 'default' by convention not required
    ...                    # <~ compute $key (yada yada)
    return $key;           # <~ key must be limited to the set of keys added with C<on>

  } $INPUT,                ### <><~ the single scalar reference to be passed to the C<dispatch> BLOCK
  ...
on

This keyword builds up the dispatch table. It consists of a static string and a subroutine reference. In order for this to work for you, the dispatch BLOCK must return strictly only the keys that are defined via on.

  my $INPUT = [qw/foo bar baz 1 3 4 5/];

  my $results = dispatch {

    my $input_ref = shift; # <~ there is only one parameter, but can a reference to anything
    my $key = q{default};  # <~ initiate the default key to use, 'default' by convention not required
    ...                    # <~ compute $key (yada yada)
    return $key;           # <~ key must be limited to the set of keys added with C<on>

  } $INPUT,                ### <><~ the single scalar reference to be passed to the C<dispatch> BLOCK
   on case1 => sub { my $INPUT = shift; ... },
   on case2 => sub { my $INPUT = shift; ... },
   on case3 => sub { my $INPUT = shift; ... },
   on case4 => sub { my $INPUT = shift; ... },
   on case5 => sub { my $INPUT = shift; ... };

Note: when the subroutine associated with each case is dispatched, the $INPUT scalar is provide as input.

  my $INPUT = [qw/foo bar baz 1 3 4 5/];

  my $results = dispatch {

    my $input_ref = shift;      # there is only one parameter, but can a reference to anything
    my $key    = q{default};    # initiate the default key to use, 'default' by convention not required
    ...                         # compute $key
    return $key;                # key must be limited to the set of keys added with C<on>

  } $INPUT,                     # <~ the single scalar reference to be passed to the C<dispatch> BLOCK
   on default  => sub {
     my $INPUT = shift;
     do_default($INPUT);
   },
   on key1     => sub {
     my $INPUT = shift;
     do_key1(cases => $INPUT);
   },
   on key2     => sub { 
     my $INPUT = shift;
     do_key2(qw/some other inputs entirely/);
   };

Diagnostics and Warnings

The on method must always follow a comma! Commas and semicolons look a lot alike. This is why a wantarray check inside is able to warn when it's being used in a useless void or scalar contexts. Experience has show that it's easy for a semicolon to sneak into a series of on statements as they are added or reorganized. For example, how quickly can you spot a the misplaced semicolon below:

  my $results = dispatch {
    my $input_ref = shift;
    ...
    return $key;
  } $INPUT,
   on case01 => sub { my $INPUT = shift; ... },
   on case02 => sub { my $INPUT = shift; ... },
   on case03 => sub { my $INPUT = shift; ... },
   on case04 => sub { my $INPUT = shift; ... },
   on case05 => sub { my $INPUT = shift; ... },
   on case06 => sub { my $INPUT = shift; ... };
   on case07 => sub { my $INPUT = shift; ... },
   on case08 => sub { my $INPUT = shift; ... },
   on case09 => sub { my $INPUT = shift; ... },
   on case10 => sub { my $INPUT = shift; ... },
   on case11 => sub { my $INPUT = shift; ... },
   on case12 => sub { my $INPUT = shift; ... },
   on case13 => sub { my $INPUT = shift; ... },
   on case14 => sub { my $INPUT = shift; ... },
   on case15 => sub { my $INPUT = shift; ... },
   on case16 => sub { my $INPUT = shift; ... },
   on case17 => sub { my $INPUT = shift; ... },
   on case18 => sub { my $INPUT = shift; ... },
   on case19 => sub { my $INPUT = shift; ... },
   on case20 => sub { my $INPUT = shift; ... };

BUGS

Please report any bugs or ideas about making this module an even better basis for doing dynamic dispatching.

AUTHOR

O. ODLER 558 <oodler@cpan.org>.

LICENSE AND COPYRIGHT

Same as Perl.