Multi-Dispatch

 view release on metacpan or  search on metacpan

lib/Multi/Dispatch.pm  view on Meta::CPAN

    multi B( Int $N                    )  { compute_B_of_even($N) }

You can also combine literal parameter constraints with types constraints
and/or C<:where> attributes, though there are admittedly fewer cases
where it is useful to do so. For example:

    multimethod set_ID (StrongPassword 'qwerty123') {
        die "You're kidding, right?"
    }

Note that, when a parameter specifies two or more kinds of constraints, those
constraints are tested left-to-right. That is: type constraints are tested
before inlined literal or expression constraints, which are in turn tested before
a C<:where> attribute.


=head2  Parameter destructuring

So far, we have seen that Multi::Dispatch can differentiate variants,
and select between them, based on the number of parameters and any
specified constraints on their values. But the module can also
distinguish between variants based on the I<structure> of their
parameters. And, in the process, extract relevant elements of those
those structures automatically.

This facility is available for parameters that expect an array reference,
or a hash reference (as those are the kinds of arguments in Perl that
can actually have some non-trivial structure).


=head3  Array destructuring

Consider a multisub that expects a single argument that is an array reference,
and responds according to the number and value of arguments in that array:

    multi handle(ARRAY $event) {
        my $cmd = $event->[0];
        if (@{$event} == 2 && $cmd eq 'delete') {
            my $ID = $event->[1];
            _delete_ID($ID);
        }
        elsif (@{$event} == 3 && $cmd eq 'insert') {
            my ($data, $ID) = $event->@[1,2];
            _insert_ID($ID, $data);
        }
        elsif (@{$event} >= 2 && $cmd eq 'report') {
            my ($ID, $fh) = $event->@[1,2];
            print {$fh // *STDOUT} _get_ID($ID);
        }
        elsif (@{$event} == 0) {
            die "Empty event array";
        }
        else {
            die "Unknown command: $cmd";
        }
    }

This code uses a single multisub with a signature, to ensure that it receives
the correct kind of argument. But then it unpacks the contents of that argument
"manually", and determines what action to take by explicitly deciphering the
structure of the argument in a cascaded S<C<if>-C<elsif>> sequence...all in that
single variant.

Avoiding that kind of all-in-one hand-coded infrastructure is the entire
reason for having multiple dispatch, so it won't come as much of a surprise
that Multi::Dispatch offers a much cleaner way of achieving the same goal:

    multi handle( ['delete',        $ID]       ) { _delete_ID($ID)             }
    multi handle( ['insert', $data, $ID]       ) { _insert_ID($ID, $data)      }
    multi handle( ['report', $ID, $fh=*STDOUT] ) { print {$fh} _get_ID($ID)    }
    multi handle( [ ]                          ) { die "Empty event array"     }
    multi handle( [$cmd, @]                    ) { die "Unknown command: $cmd" }

Instead of specifying the single argument as a scalar that must be an array
reference, each variant in this version of the multisub specifies that single
argument as an anonymous array (i.e. as an actual array reference), with zero or
more B<I<subparameters>> inside it. These subparameters are then matched (for
number, type, value, etc.) against each of the elements of the arrayref in the
corresponding argument, in just the same way that regular parameters are matched
against a regular argument list.

If the contents of the argument arrayref match the specified subparameters,
the argument as a whole is considered to have matched the parameter as a whole,
and so the variant may be selected.

Thus, in the preceding example:

=over

=item *

If the single arrayref argument contains exactly two elements,
the first of which is the string C<'delete'>, then the first variant
will be selected.

=item *

If the arrayref contains exactly three elements, the first being the string
C<'insert'>, then the second variant will be selected.

=item *

If the arrayref contains either two or three elements, the first being the string
C<'report'>, then the third variant will be selected.

=item *

If the arrayref contains no elements, then the fourth variant will be selected.

=item *

If the arrayref contains at least one element, but any number of extras (which
are permitted because they will be assigned to the anonymous slurpy array
subparameter), then the fifth variant will be selected.

=back

In other words, destructured array parameters allow you to "draw a picture"
of what an arrayref parameter should look like internally, and have the multisub
or multimethod work out whether the actual arrayref argument has a compatible
internal structure.

lib/Multi/Dispatch.pm  view on Meta::CPAN

    multi handle( ['report', $ID =~ /^X\d{6}$/, GLOB $fh = *STDOUT] ) {...}
    #                            ↑↑↑↑↑↑↑↑↑↑↑↑↑  ↑↑↑↑

All these variants still expect a single arrayref as their argument, but now the
contents of that arrayref must conform to the various constraints specified on
the corresponding subparameters.

Array destructuring is particularly useful in pure functional programming.
For example, here's a very clean implementation of mergesorting, with no
explicit control structures whatsoever:

    multi merge ( [@x],     []             )  { @x }
    multi merge ( [],       [@y]           )  { @y }
    multi merge ( [$x, @x], [$y <= $x, @y] )  { $y, merge [$x, @x], \@y }
    multi merge ( [$x, @x], [$y >  $x, @y] )  { $x, merge \@x, [$y, @y] }

    multi mergesort (@list <= 1) { @list }
    multi mergesort (@list >  1) {
        merge
            [ mergesort @list[      0..@list/2-1] ],
            [ mergesort @list[@list/2..$#list]    ]
    }


=head3  Hash destructuring

Arrayref destructuring is extremely powerful, but the ability to specify
destructured hashref parameters is even more useful.

For example, passing complex datasets around in tuples is generally considered a
bad idea, because positional look-ups (C<< $event->[2] >>, C<< $client->[17] >>)
are considerably more error-prone than named look-ups
(C<< $event->{ID} >>, C<< $client->{overdraft_limit} >>)

So it's actually quite unlikely that the C<handle()> multisub used as an
example in the previous section would pass in each event as an arrayref. It's
much more likely that an experienced programmer would structure events as
hashrefs instead:

    multi handle(HASH $event) {
        if ($event->{cmd} eq 'delete') {
            _delete_ID($event->{ID});
        }
        elsif ($event->{cmd} eq 'insert') {
            _insert_ID($event->@{'ID', 'data'});
        }
        elsif ($event->{cmd} eq 'report') {
            print {$event->{fh} // *STDOUT} _get_ID($event->{ID});
        }
        elsif (exists $event->{cmd}) {
            die "Unknown command: $event->{cmd}";
        }
        else {
            die "Not a valid event";
        }
    }

While this is a arguably little cleaner than the array-based version,
and certainly a lot safer I<(are you B<sure> all the array indexes
were correct in the array-based version???)>, it still suffers from
the "all-in-one-cascade" problem.

Fortunately, Multi::Dispatch can also destructure hashref parameters,
allowing them to be specified as destructuring anonymous hashes:

    multi handle( { cmd=>'delete', ID=>$ID }                    ) {...}
    multi handle( { cmd=>'insert', ID=>$ID, data=>$data }       ) {...}
    multi handle( { cmd=>'report', ID=>$ID, fh=>$fh = *STDOUT } ) {...}
    multi handle( { }                                           ) {...}
    multi handle( { cmd=>$cmd, % }                              ) {...}

Within a destructuring hash, each subparameter is specified as a
S<B<I<key>>C<< => >>B<I<value>>> pair, with the keys specifying the
keys to be expected within the corresponding hashref argument,
and the values specifying the subparameter variables into which
the corresponding values from the hashref argument will be assigned.

Unlike destructuring arrays, the order in which subparameters are
specified in a destructuring hash doesn't matter. Each entry from
the hashref argument is matched to the corresponding subparameter
by its key.

Another important difference is that, if you want to specify a
destructuring hash that can match a hashref argument with extra
keys, you need to specify a named or anonymous slurpy hash
as the final subparameter I<(as in the final variant in the
preceding example)>. Without a trailing slurpy subparameter,
a destructuring hash will only match a hashref argument that
has exactly the same set of keys as the destructuring hash itself.

As with destructuring array parameters, the subparameters of
destructuring hashes can take advantage of all the features
of regular parameters (required/optional, copy/alias, constraints, etc.).
So, this version of the C<handle()> multisub could still impose
all the additional constraints that were previously required:

    multi handle( {cmd => 'delete', ID => $ID =~ /^X\d{6}$/} )
    {...}

    multi handle( {cmd => 'insert', ID => $ID =~ /^X\d{6}$/, data => \%data} )
    {...}

    multi handle( { cmd => 'report',
                    ID  => $ID =~ /^X\d{6}$/,
                    fh  => GLOB $fh = *STDOUT
                  } )
    {...}

As a second example, consider the common way of cleanly passing named
optional arguments to a subroutine: bundling them into a single hash reference:

    my @sorted = mysort({foldcase=>1}, @unsorted);

    my @sorted = mysort({reverse=>1, unique=>1}, @unsorted);

    my @sorted = mysort({key => sub { /\d+$/ ? $& : Inf }}, @unsorted);

The subroutine then pulls these options out of the corresponding C<$opts>
parameter by name:

    sub mysort ($opts, @data) {



( run in 1.728 second using v1.01-cache-2.11-cpan-39bf76dae61 )