NAME
Multi::Dispatch - Multiple dispatch for Perl subs and methods
VERSION
This document describes Multi::Dispatch version 0.000002
SYNOPSIS
use Multi::Dispatch;
# Create a mini Data::Dumper clone that outputs in void context...
multi dd :before :where(VOID) (@data) { say &next::variant }
# Format pairs and array/hash references...
multi dd ($k, $v) { dd($k) . ' => ' . dd($v) }
multi dd (\@data) { '[' . join(', ', map {dd($_)} @data) . ']' }
multi dd (\%data) { '{' . join(', ', map {dd($_, $data{$_})} keys %data) . '}' }
# Format strings, numbers, regexen...
multi dd ($data) { '"' . quotemeta($data) . '"' }
multi dd ($data :where(\&looks_like_number)) { $data }
multi dd ($data :where(Regexp)) { 'qr{' . $data . '}' }
multi dd ($data :where(GLOB)) { "" . *$data }
use Object::Pad; # or use feature 'class', when it's available
class MyClass {
field $status;
multimethod status () { return $status }
multimethod status ($new_status) { $status = $new_status }
multimethod status ("") { die "New status cannot be empty" }
}
DESCRIPTION
This module provides two new keywords: multi
and multimethod
which allow you to define multiply dispatched subroutines and methods with sophisticated signatures that may include aliasing, context constraints, type constraints, value constraints, argument destructuring, and literal value matching.
Multisubs
The keyword multi
declares a multisub: a multiply dispatched subroutine. You can declare two or more multisub variants with the same name, as long as they have distinct signatures. For example, here are three variants of the expect()
multisub:
multi expect ($expected, $msg) { die $msg if !$expected }
multi expect (&expected, $msg) { die $msg if !expected() }
multi expect ($expected ) { die "Unexpected error" if !$expected }
With those declarations in place, the following calls to expect()
will each invoke a different variant:
expect( $x > 0 ); # Invokes 3rd variant
expect( $x > 0, 'Expected positive $x'); # Invokes 1st variant
expect(sub { $x > 0 }, 'Expected positive $x'); # Invokes 2nd variant
Calling expect()
without arguments or with more than two arguments produces an exception indicating that there was no suitable variant that could handle the specified argument list.
Multimethods
The keyword multimethod
declares a multiply dispatched instance method. You can declare two or more multimethod variants with the same name, as long as they have distinct signatures. For example:
package MyClass {
multimethod name () { return $self->{name} }
multimethod name ($new_name) { $self->{name} = $new_name }
...
}
Now any call to $obj->name()
without an argument simply returns the current value of the 'name'
entry in the hash-based object. Whereas, calling $obj->name($value)
with a single argument assigns that argument to the same hash entry.
Calling $obj->name($value1, $value2)
with two (or more) arguments produces an exception indicating that there was no suitable variant of name()
that could handle the specified argument list.
Note that every multimethod has an implicit first parameter ($self
) which is automatically assigned a reference to the invocant object.
Multi::Dispatch can also be used in classes created using the Object::Pad module (and, eventually, using the new built-in class
mechanism):
use Object::Pad;
class MyClass {
field $name;
multimethod name () { return $name }
multimethod name ($new_name) { $name = $new_name }
...
}
In such cases, the underlying dispatcher will be implemented as a proper Object::Pad method
, rather than as a simple Perl sub
.
Note that all subsequent class-based examples in this document will be shown using the Object::Pad/use experimental 'class'
syntax, but would all work equally well using the classic Perl package
/sub
OO mechanism.
Multimethod inheritance
A multimethod spans all the base classes of its own class. That is: a multimethod in a derived class inherits the variants defined in all of its base classes. For example:
class Account {
field $balance :reader;
multimethod debit ($amount :where({$amount <= $balance})) {
$balance -= $amount;
}
multimethod debit ($amount :where({$amount > $balance}) {
die "Insufficient funds";
}
}
class Account::Overdraft :isa(Account) {
field $overdraft;
multimethod debit ($amount :where({$amount > $self->balance})) {
my $balance = $self->balance;
$self->debit($balance);
$overdraft += $amount - $balance;
}
}
When called on an object of class Account::Overdraft, the debit()
multimethod has access to three variants: the one defined in its own class, and the two inherited from Account.
The variant defined in Account::Overdraft overrides the :where({$amount > $balance})
variant inherited from Account, because derived variants always preempt base class variants with the same number of arguments and constraints (see "How variants are selected for dispatch").
Note, however, that the inherited :where({$amount <= $balance})
variant continues to be available, because its constraint is mutually exclusive with that of the derived variant.
:common
multimethods
Multimethods are normally per-object methods, but they can be declared as per-class methods instead, by including the :common
attribute in their declaration. For example:
class Sequence {
field $from :param;
field $to :param;
field $step :param;
multimethod of :common ($to) {
$class->new(from=>0, to=>$to-1);
}
multimethod of :common ($from, $to) {
$class->new(from=>$from, to=>$to);
}
multimethod of :common ($from, $then, $to) {
$class->new(from=>$from, to=>$to, step=>$then-$from);
}
}
The Sequence class declares three variants of the of()
multimethod, each of which allows the user to call them on the class, rather than on an object:
$seq = Sequence->of(100); # 0..99
$seq = Sequence->of(1, 99); # 1..99
$seq = Sequence->of(1, 3, 99); # 1, 3, 5,...99
Note that every :common
multimethod has an implicit first parameter ($class
), which is automatically assigned a string containing the name of the class through which it was invoked. Such multimethods don't have an automatic $self
parameter. This is true, even if the :common
multimethod is invoked on a class instance (i.e. an object), rather than on the class itself.
Multimethod inheritance of non-multi methods
As explained earlier, a multimethod in a derived class inherits all the variants of the same name from all its parent classes, and considers all of them when dispatching a call.
If a parent class declares a non-multi method of the same name, that method would normally not be considered when a call to the multimethod is dispatched...because the inherited method isn't an inherited variant; it's a separate method that has been overridden by the derived-class multimethod of the same name.
For example, if the base class Debitable defines a debit()
method:
class DebitReporter {
method debit($amount) { _report_deposit_attempt($amount) }
...
}
class Account :isa(DebitReporter) {
field $balance :reader;
multimethod debit ($amount :where({$amount <= $balance})) {
$balance -= $amount;
}
multimethod debit ($amount :where({$amount > $balance}) {
die "Insufficient funds";
}
}
...then that base-class method would not be considered as a dispatch target when an object of the derived class Account calls $acct_obj->debit($amount)
and thereby invokes the derived class's debit()
multimethod. Because method DebitReporter::debit()
isn't a variant of multimethod Account::debit()
.
However, in such situations, Multi::Dispatch recognizes the relationship between the base-class method and the derived-class multimethod, and uses the inherited (non-multi) method as a fallback, if no variant of the derived-class multimethod can be selected for dispatch.
In other words, each multimethod has an extra implicit variant that attempts to redispatch calls in the traditional Perl OO fashion (i.e. via next::method
). So, for example, Account's debit()
multimethod effectively has an automatically supplied extra lowest-precedence variant:
# Implicitly added variant...
multimethod debit (@args) { $self->next::method(@args) }
Note that, in such cases, the inherited non-multi method that is selected as the fallback is determined entirely by the standard behaviour of next::method
; That is, by the current use mro
semantics in effect within the derived class.
Note too, that if you want to disable (or change) this fallback behaviour, you can just explicitly define your own low-precedence variant in the derived class. For example:
# Explicitly switch off fallback behaviour...
multimethod debit (@args) { die "Can't debit @args" }
Multisub and multimethod signatures
Multisubs and multimethods select which variant to call based on how well a given argument list matches the signature of each variant.
A variant's signature consists of the cumulative number, constraints, structure, requiredness, optionality, and slurpiness of its various parameter variables.
The details of how a particular variant is selected are given in "How variants are selected for dispatch" but, in general, the dispatch mechanism favours the most extensive, precise, specific, and constrained signature that is compatible with the actual argument list.
For example, if a multisub has two variants:
multi handle(@args) {...}
multi handle(Int $count, \%data :where({exists $data{name}) {...}
...then a call such as:
handle(7, {name=>'demo', values=>[1,2,3]})
...would be compatible with the signatures of both variants, but will be dispatched to the second variant, because that variant defines a signature that is more extensive (two parameters vs one), more precise (exactly two arguments required vs any number allowed), more specific (the first argument must be an integer and the second argument must be a hashref), and more constrained (the hashref must have a 'name'
key).
In order to allow for this kind of precision and specificity in signatures, the module provides a large number of parameter features (far larger than Perl's built-in subroutine signatures). These are described in the following sections.
Required parameters
Any scalar parameter that is included in a variant's signature, and which does not have a default value specified (see "Optional parameters") is treated as a required parameter.
When a multisub or multimethod is called with N arguments, only those of its variants with at most N required parameters will be considered for final dispatch. Variants with fewer than N required parameters may also be considered if they have additional optional or slurpy parameters to which the extra arguments can be assigned.
For example, given the following declarations:
multi compare ($x, $y) {...}
multi compare ($x, $y, $op) {...}
...the compare()
multisub can only be successfully called with either two or three arguments. Any other number of arguments will produce a "no suitable variant" exception.
Optional parameters
A required parameter may be made optional by appending an =
after the parameter name, followed by an expression that produces a suitable default value. For example:
multi check ($test, $msg = 'Failed check') { croak $msg if !$test; }
multi check (&test, $msg = 'Failed check') { croak $msg if !test(); }
Now the check()
multisub may be called with either one or two arguments, and the second parameter will be "filled in" with the default string if only one argument is passed.
Note that, if a multisub or multimethod has variants with either required or optional arguments, the variant with the greater number of required arguments will be preferred. For example, given:
multimethod handle ($event, $comment = '???') {...}
multimethod handle ($event, $comment ) {...}
...a two-argument call to:
$obj->handle($event, 'Normal event');
...will always call the second variant, because all of that variant's parameters are required, whereas the second parameter of the first variant is only optional.
Note that, as with regular Perl subroutine signatures, all optional parameters in the signature of a multi
or multimethod
must come after any required parameters, and before any final "slurpy" parameter.
return
as a default value
Perl's built-in signature mechanism for subroutines allows any parameter default expression to include a return
statement. For example:
sub name ($new_name = return $old_name) {
$old_name = $new_name;
}
This means that if the name()
subroutine is called without an argument, it immediately returns the value of $old_name
, without bothering to invoke the body of the subroutine.
The multisubs and multimethods provided by Multi::Dispatch do not allow parameter default values to include a return
, because this would interfere with the variant selection process, which must bind every compatible variant's signature to the argument list, evaluating defaults as it goes, before it decides which variant to dispatch to. Encountering a return
in the middle of that process would short-circuit the variant-selection process, leading to incorrect dispatches.
Hence, Multi::Dispatch detects the presence of a return
statement within a parameter default and issues a compile-time error.
Note that the use of return
statements in parameter defaults is usually just a workaround for the lack of multiple dispatch in standard Perl. The correct way to accomplish the same effect with multiple dispatch is to define two variants, like so:
sub name () { return $old_name }
sub name ($new_name) { $old_name = $new_name }
Slurpy parameters
So far, all the parameters specified in a variant's signature must be scalars (either required or optional). However, the final parameter in a variant's signature may also be specified as either an array or a hash, in which case all of the remaining arguments not yet assigned to a preceding parameter are slurped up into that final array or hash. Such a final parameter is therefore known as a "slurpy" parameter. (This feature is also available in regular Perl subroutine signatures):
multi sublist( $from, $to, @list ) {...}
# ↑↑↑↑↑
multi tidy( $str, %options) {...}
# ↑↑↑↑↑↑↑↑
If the final slurpy parameter is an array, it will consume as many extra arguments as are left in the argument list (or none, if the entire argument list has already been allocated to preceding parameters).
If the final slurpy parameter is a hash, it will treat any remaining arguments as a list of key =>
value pairs, and use that list to initialize the slurpy hash. If the number of remaining arguments is odd, this will throw a run-time exception (just as a regular Perl subroutine signature would).
As with regular Perl subroutine signatures, the final slurpy of a variant cannot be given a default value.
Anonymous parameters
In some cases you may need to ensure a parameter is passed to a multisub or multimethod, but you may not care what value the corresponding argument had. In such cases, you can leave out the actual name of the parameter, and merely specify its sigil.
For example:
class Event {
multimethod handle ($timestamp, @log_msgs) {
$log->report(time=>$timestamp, msg=> "@log_msgs");
...
}
}
class Event::Unlogged :isa(Event) {
multimethod handle ($, @) {
...
}
}
Here the derived Event::Unlogged has no need of the arguments passed to its handle()
method, except that (to preserve Liskov Substitutability) they must still be present. So, in the derived class, those parameters can be specified as being anonymous, which ensures that their presence will still be verified, but that no parameter variables will be allocated or initialized.
Note that any kind of simple parameter (scalar, array, hash, code) may be declared anonymous. However, aliased parameters (see "Aliased Parameters") may not.
Anonymous scalar parameters may also be specified as optional:
multimethod handle ($=undef, @) {
...
}
Just as in regular Perl subroutine signatures, an anonymous optional parameter may also omit the actual default value entirely:
multimethod handle ($=, @) {
...
}
Aliased parameters
Arguments passed to a multisub or multimethod are normally copied into the relevant parameters. However, you can also pass arguments as references, by placing a backslash in front of the parameter:
multi foo (\$s, \@a, \%h) {...}
Each such parameter expects to be passed a reference to the corresponding type, and aliases that reference to the parameter using the built-in "refaliasing" mechanism (see perlref).
So the foo()
multisub defined above could be called like so:
foo(\$name, \@scores, \%options)
...in which case the $s
parameter would be aliased to the $name
variable, the @a
parameter would be aliased to the @scores
variable, and the %h
parameter would be aliased to the @options
variable (but please choose better parameter names in real life!)
An aliased parameter can be bound to any form of reference of the appropriate kind, so you could also call foo()
like so:
foo(\'my name', [1..99], {quiet=>1, overwrite=>0})
Note that any argument intended to be passed to an aliased parameter must be a reference of the appropriate type. If it isn't, the entire variant will be excluded from the dispatch process.
Note too that, because each aliased parameter is bound to a reference (i.e. a scalar value) you can specify as many array- or hash-alias parameters as you wish. For example:
multi merge(\@listA, \@listB) {
return !@listA ? @listB
: !@listB ? @listA
: $listA[0] < $listB[0] ? ($listA[0], merge(\@listA[1..$#listA], \@listB)
: ($listB[0], merge(\@listA, \@listB[1..$#listB])
}
merge(\@left, \@right);
Aliased array and hash parameters are not slurpy in nature. Each such parameter expects exactly one array reference or one hash reference only. Of course, you can still place a single unaliased slurpy array or hash after one or more aliased arrays or hashes, to sop up any extra arguments.
Optional aliased parameters
Aliased parameters can also be specified as optional, in the usual way (with a trailing =
and a default value):
multi handle (\$event = \undef, \@data = [], \%options = {}) {...}
However, for obvious reasons, the default value should be a reference of the appropriate kind. If it isn't, the non-reference default value will cause the variant to immediately be excluded from the dispatch process (because the non-reference default value cannot be aliased to the parameter).
This will either cause a different variant to be selected, or else (if there is no other compatible variant) a run-time "no suitable variant" exception will be thrown.
Codelike parameters
Unlike Perl's regular subroutine signatures, multisubs and multimethods can specify parameters that are subroutines. For example:
# Functional composition: f ∘ g
multi compose (&f, &g) { ... }
# ↑↑ ↑↑
# Callbacks...
multimethod handle ($event, &on_success, &on_failure) {...}
# ↑↑↑↑↑↑↑↑↑↑↑ ↑↑↑↑↑↑↑↑↑↑↑
Code parameters expect an argument that is a subroutine reference:
my $normalize = compose( \&casefold, \&uniq );
# ↑↑↑↑↑↑↑↑↑↑ ↑↑↑↑↑↑
$obj->handle($next_event, sub {$success++}, sub {die "failed: @_'});
# ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑ ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑
Each code parameter creates a lexical subroutine within the block of its variant, and that lexical subroutine can then be called in the usual way within the variant to invoke the corresponding coderef argument:
multi compose (&f, &g) { return sub { f(g(@_)) }
# ↑↑↑↑↑↑↑↑
multimethod handle ($event, &on_success, &on_failure) {
...
if ($handled) { on_success($event) }
else { on_failure($event) }
# ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑
}
You can also specify code parameters as aliases:
multi compose (\&f, \&g) { return sub { f(g(@_)) }
multimethod handle ($event, \&on_success, \&on_failure) {
...
if ($handled) { on_success($event) }
else { on_failure($event) }
}
As you see, in most cases aliased code parameters behave exactly the same as unaliased code parameters...except if you take the address of the parameter:
# This version is broken...
multi call_once_bad (&fn) {
state %already_called;
die "Can't call that twice" if $already_called{\&fn}++;
goto &fn; # ↑↑↑↑
}
# This version works as expected...
multi call_once_good (\&fn) {
state %already_called;
die "Can't call that twice" if $already_called{\&fn}++;
goto &fn; # ↑↑↑↑
}
The first version is broken because each call to call_once_bad()
effectively copies the subroutine argument (&fn
) to a new lexical subroutine, which may have a different address in every call...and will definitely have a different address from the original argument.
The second version works as intended because, in call_once_good()
, the original subroutine-reference argument is aliased to &fn
, so &fn
has the same address as the argument itself.
Parameter constraints
So far, the different variants of a multisub or multimethod have been distinguished solely by the number and kind of parameters they define.
However, it is also possible to define two or more variants with the same number of arguments, so long as they are distinguished in some other way. One way two variants can be distinguished is by the constraints placed upon their parameters.
A parameter constraint specifies that the value of the corresponding argument must meet some condition. If any argument does not meet the condition of its parameter, the variant is immediately rejected during the dispatch process.
Multi::Dispatch allows parameters to be specified with two different kinds of constraints: type tests and value tests, In keeping with long Perl tradition, the module provides multiple mechanisms and multiple syntaxes for specifying these tests. Specifically, parameter constraints can be specified via a prefix typename or classname, via an infix expression on the parameter, or via a postfix :where
attribute.
Prefix type constraints
If a parameter variable is preceded by an identifier, that identifier is taken to be the name of a class, type, or "reftype", and the corresponding argument must be compatible with that type, or else dispatch to the variant will be rejected.
If the prefix identifier is entirely uppercased, then it is treated as a Perl reference type and the corresponding argument must be of the same referential type. That is, the argument must satisfy the constraint: reftype($arg) eq 'REFTYPE'
, where 'REFTYPE'
is one the type descriptor strings returned by Scalar::Util::reftype()
or builtin::reftype()
.
For example, the following multisub defines three variants that can accept (only) references to arrays, hashes, or subroutines:
multi report (ARRAY $data) {...}
multi report (HASH $data) {...}
multi report (CODE $data) {...}
As a special case of this kind of constraint, if the prefix identifier is OBJ
:
multi report (OBJ $data) {...}
...then it specifies that the corresponding argument must be an object of some user-defined class. In other words, the argument must satisfy the constraint: blessed($arg)
. (Note that Perl's built-in qr//
anonymous regexes are deliberately not accepted by the OBJ
constraint, because most people think of regexes as simple values, rather than as objects.)
Otherwise, if the prefix identifier is a Type::Tiny typename, it is treated as a type, and the corresponding argument must satisfy the constraint: TypeName->check($arg)
. Note, however, that this test is always inlined, so no extra method call is actually involved.
For example, the following multimethod defined five variants that can accept a variety of types of argument:
use Types::Standard ':all';
multimethod add (Int $i ) {...}
multimethod add (StrictNum $n ) {...}
multimethod add (Str $s ) {...}
multimethod add (ArrayRef[Num] $aref) {...}
multimethod add (FileHandle $fh ) {...}
Note that, in order to use such types, the specified type must already be defined in the scope where the multisub or multimethod is defined. Typically this means that the Types::Standard module (or another module providing Type::Tiny types, such as Types::Common::Numeric or Types::Common::String) must already have been loaded in that scope.
If the prefix identifier is not a reference type or a defined Type::Tiny typename, it will be treated as the name of a class, and the corresponding argument must satisfy the constraint: blessed($arg) && $arg->isa('Class::Name')
. That is, the argument must be an object (as defined by Scalar::Util::blessed()
or builtin::blessed()
) of the specified class...or of one of its derived classes.
For example, the following multimethod provides three variants that can accept either a Status::Message object, or an Event::Result object or a Transaction object:
multimethod update_status(Status::Msg $m) {...}
multimethod update_status(Event::Result $r) {...}
multimethod update_status(Transaction $t) {...}
There is some overlap in the capabilities of these three type-specification mechanisms, so it is often possible to specify a particular type constraint in multiple ways. For example:
multi filter (Regexp:: $pat, IO::File $fh) {...} # Blessed objects
multi filter (REGEXP $pat, GLOB $fh) {...} # Builtin reftypes
multi filter (RegexpRef $pat, FileHandle $fh) {...} # Type::Tiny types
Generally, constraints based on built-in reftypes are the quickest to verify, but Type::Tiny types are more robust and reliable, whilst classnames will be most appropriate in predominantly OO code. If you specify separate variants with all three kinds of type constraint (as in the preceding example), Type::Tiny types will take precedence over class types, which take precedence over reftypes. Hence, in the preceding example, the filter(RegexpRef, FileHandle)
variant will always be selected over the other two variants.
The precedence of Type::Tiny types over classnames also applies if a given type specifier is both a Type::Tiny typename and a Perl classname. For example, given the following definitions:
class Value { field $val :param :reader; }
use Types::Standard 'Value';
multi report(Value $v) { say $v }
...the type-constraint on $v
will be Value->check($v)
, not $v->isa('Value')
. If you want an ambiguous type specifier to be interpreted as a classname instead, either specify it that way using Type::Tiny:
use Types::Standard 'InstanceOf';
multi report(InstanceOf['Value'] $v) { say $v }
...or else just append a ::
to the type specifier to mark it unambiguously as a Perl classname:
multi report(Value:: $v) { say $v }
Class and type precedence
There is a broader issue of type precedence than just class vs type vs reftype. Even when you are just using classes or just using Type::Tiny types, two or more variants may both be valid alternatives. For example, if two or more classname parameter constraints are in the same type hierarchy:
multi handle (Event $e) {...}
multi handle (Event::Priority $e) {...}
multi handle (Event::Priority::Urgent $e) {...}
...or if two or more parameter types are subtypes and supertypes:
multimethod add (Int $i) {...}
multimethod add (StrictNum $n) {...}
multimethod add (Str $s) {...}
multimethod add (Value $v) {...}
If Event and Event::Priority are base classes of Event::Priority::Urgent, then a call to handle($urgent_priority_event)
will satisfy the parameter constraints of all three variants, as an Event::Priority::Urgent object isa
Event::Priority object and also isa
Event object. Likewise, a call to $obj.add(42)
will satisfy all four variants, as 42 is an integer, a strict number, and a value, and can be trivially coerced to a string.
The question then is how Multi::Dispatch selects the variant to be called when two or more class/type constraints are equally well satisfied. The answer is that Multi::Dispatch chooses the variant with the "most specific" constraint. For classes, a derived class is "more specific" than all its base classes, so the module prefers the variant with the most-derived class constraint. For types, Type::Tiny defines an is_subtype_of()
method, and Multi::Dispatch chooses the variant whose type constraint is a subtype of all of the others.
The effect of these tie-breaking rules is that, generally, you simply get the most specifically applicable variant for the actual type/class of argument passed. Or, in other words: the least amount of surprise.
Ordering type-constrained variants like this is relatively easy when there is only a single typed parameter involved. But things rapidly get more complex when two or more parameters have class or type constraints. For example, consider the following two situations:
# Compare events...
multi compare_events (Event $e1, Event::Priority $e2) {...}
multi compare_events (Event::Priority $e1, Event $e2) {...}
compare_events( Event::Priority->new, Event::Priority->new );
# Implement a ternary $from <= $x <= $to operator...
multi contains (Num $x, Int $from, Int $to) {...}
multi contains (Int $x, Num $from, Num $to) {...}
my $in_range = contains($n, 0, 9);
In each case, the constraints of both available variants are satisfied, but which one will be called?
If we just consider the constraint on the first parameter, then clearly the second variant is a better match. But if we consider the second parameter's constraints, then the first variant is more specific (as it also is for the third parameter of contains()
).
Hence, a set of variants can be intrinsically unordered where there are two or more type-constrained parameters. Any language that supports multiple dispatch based on argument types must handle these kinds of situations, but they may do so in quite different ways. Some languages simply proceed left-to-right and choose according to the leftmost constraint where a clear ordering is detected. Others add up the number of most-specific constraints in each variant and select the variant with the highest total. Others detect the inherent conflict and produce either a compiler error or a run-time exception.
Multi::Dispatch simply detects that the two variants are not sortable by their type constraints, and silently falls back on other means of selecting between them (as described in "How variants are selected for dispatch"). In the above cases, this would result in the first variant of each multisub being called (because the variants are not distinguishable by the structure, number, or requiredness of their parameters, so the module sorts them earliest-declaration-first).
Note that in such situations, if the fallback selection doesn't do what you want, the correct solution is to provide yet another variant; one that is unambiguously more precise than the existing choices. For example:
# Variant in which both constraints are most specific...
multi compare_events (Event::Priority $e1, Event::Priority $e2) {...}
# Variant in which all three constraints are most specific...
multi contains (Int $x, Int $from, Int $to) {...}
Anti-type prefix constraints
Instead of specifying that a parameter must satisfy a specific type-constraint, you can also specify that a particular parameter must not satisfy a specific constraint. For example, you can specify that a parameter must not be an integer, or not a regex, or not an object of a particular class.
You can specify this kind of "antitype" for all three kinds of prefix type constraints, simply by prefixing the type-specifier with a !
, like so:
# First argument must NOT be a regular expression...
# ↓
multi filter( !Regexp:: $str, IO::File $fh ) {...}
multi filter( !REGEXP $str, GLOB $fh ) {...}
multi filter( !RegexpRef $str, FileHandle $fh ) {...}
# The argument must NOT be an instance of the Value class...
# ↓
multi report( !InstanceOf['Value'] $v ) { say $v }
multi report( !Value:: $v ) { say $v }
# Second argument must NOT be a hash of integers...
# ↓
multi hoi_map( Code $block, !HashRef[Int] $data ) {
die "hoi_map() requires a hash of integers";
}
Postfix :where
blocks
Type constraints focus on the kind of argument that is passed to a parameter, but you can also test the actual value of an argument as a parameter constraint, by specifying a :where
attribute immediately after the parameter name. For example:
# ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
multi factorial ($n :where({$n < 2})) { 1 }
multi factorial ($n :where({$n >= 2})) { $n * factorial($n-1); }
# ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
multi alert ($msg :where({length($msg) == 0})) {}
multi alert ($msg :where({length($msg) > 0})) { Alert->new($msg)->raise }
# ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
multimethod deposit ( @amounts :where({sum(@amounts) < 0}) ) {
die "Can't deposit a negative total: use withdraw() instead.";
}
# ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
multimethod deposit ( @amounts :where({sum(@amounts) > 10000}) ) {
die "Can't deposit a large total: use report_deposit() instead.";
}
# ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
multimethod deposit (@amounts :where({0 < sum(@amounts) < 10000}) ) {
$balance += $_ for @amounts;
}
Note that in each case, the condition in the parens of the :where
attribute is a block of code, which tests some property of the value assigned to the corresponding parameter. A variant is rejected as a candidate for dispatch if any of its parameter's :where
blocks returns a false value.
:where
blocks can also refer to other parameters declared earlier in the variant's parameter list:
# Swap back range boundaries if they were passed in the wrong order...
# ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
multimethod set_range($from, $to :where({$to > $from}) ) { ($min, $max) = ($from, $to) }
multimethod set_range($from, $to :where({$to <= $from}) ) { ($min, $max) = ($to, $from) }
...or to external variables:
# Ignore alerts if global $SILENT variable is set...
# ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
multi alert ($msg :where({$SILENT})) {}
...or even to stateful operators or functions:
# Ignore alerts if non-interactive input or closed output...
# ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
multi alert ($msg :where({ not -t *STDOUT })) {}
multi alert ($msg :where({ !eof() })) {}
Note that, because variants with :where
constraints are considered for dispatch ahead of variants without constraints, you can dispense with the parameter constraint on a subsequent variant if that constraint is mutually exclusive with the constraint on the equivalent parameter in a preceding variant. For example:
multi factorial ($n :where({$n == 0})) { 1 }
multi factorial ($n ) { $n * factorial($n-1); }
multi alert ($msg ) {}
multi alert ($msg :where({length($msg) > 0})) { Alert->new($msg)->raise }
multimethod deposit ( @amounts :where({sum(@amounts) < 0}) ) {...}
multimethod deposit ( @amounts :where({sum(@amounts) > 10000}) ) {...}
multimethod deposit ( @amounts ) {...}
multimethod set_range($from, $to ) {...}
multimethod set_range($from, $to :where({$to <= $from}) ) {...}
Postfix :where
values
A common use of :where
blocks is to select a special behaviour for particular argument values. For example:
multi factorial ($n :where({$n == 0})) { 1 }
multi factorial ($n ) { $n * factorial($n-1) }
multi alert ($msg :where({$msg eq ""})) {}
multi alert ($msg ) { Alert->new($msg)->raise }
This is somewhat tedious and potentially error prone. So :where
constraints can also be specified as a literal value: a number, a string, a regular expression, an undef
, a sigiled subroutine name, or a class or type name. When specified with such a value, a :where
attribute smart-matches the parameter variable against that value, as follows:
:where(12345)
––––>:where({ $PARAM == 12345 })
:where('str')
––––>:where({ $PARAM eq 'str' })
:where(/pat/)
––––>:where({ $PARAM =~ /pat/ })
:where(undef)
––––>:where({ !defined($PARAM) })
:where(\&fun)
––––>:where({ fun($PARAM) })
:where(X::IO)
––––>:where({ $PARAM->isa(X::IO) })
:where(Value)
––––>:where({ Value->check($PARAM) })
:where(ARRAY)
––––>:where({ reftype($PARAM) eq 'ARRAY' })
So, for example, the previous examples could also be specified like so:
multi factorial ($n :where(0)) { 1 }
multi factorial ($n ) { $n * factorial($n-1) }
multi alert ($msg :where("")) {}
multi alert ($msg ) { Alert->new($msg)->raise }
Infix expression constraints
:where
values parameters simplify the specification of constraints that require an argument to be a specific value or type, but many constraints involve operations other than some form of identity, equality, or matching. For example:
multi alert ($msg :where({ $msg ne "" })) {...}
multi factorial ($n :where({ $n > 0 }) ) {...}
multi add_ID ($ID :where({ $ID !~ /X\w{4}\d{6}/ })) {...}
multimethod set_range($from, $to :where({ $to > $from }) ) {...}
multimethod debug ($obj :where({ $obj->DOES('Debugging') }) ) {...}
As all those examples illustrate, these expressions frequently involve the parameter variable as the left operand. In such cases, Multi::Dispatch allows you to simplify and de-noise the entire parameter specification by appending the operator and right operand directly after the declaration of the parameter itself:
multi alert ($msg ne "") {...}
multi factorial ($n > 0) {...}
multi add_ID ($ID !~ /X\w{4}\d{6}/) {...}
multimethod set_range($from, $to > $from) {...}
multimethod debug ($obj -> DOES('Debugging')) {...}
The constraint expression can be as complex as you wish:
multi factorial ($n > 0 && $n < 200) {...}
multimethod set_range($from, $to > $from > 0) {...}
Note, however, that in these kinds of constraints, the parameter being declared must be the leftmost element of the constraining expression. For example, you can't declare the constrained $to
parameter of set_range()
like so:
# Not a valid parameter declaration here (attempts to redeclare $from)
multimethod set_range($from, $from < $to) {...}
Generally, it's better to confine such inlined constraints to single simple arithmetic or string operators:
multi factorial ($n == 0) { 1 }
multi factorial ($n > 0) { $n * factorial($n-1) }
multi factorial ($n < 0) { die "Can't take the factorial of a negative number" }
multi alert ($msg eq "") {}
multi alert ($msg ne "") { Alert->new($msg)->raise }
More complicated constraints are usually easier to detect and understand within the code if they're visually quarantined in a :where
attribute:
multimethod set_range($from, $to :where({0 < $from < $to})) {...}
multimethod debug ($obj :where({$obj->DOES('Debugging')})) {...}
Literal value parameters
:where
attributes and inline constraint expressions allow any computable constraint to be applied to any parameter of any variant. But even simple inlined constraints aren't always as clean or as readable as we might wish. Especially when the constraint is testing whether an argument is a particular value. There's still a lot of visual noise in declarations such as:
multi factorial ($n == 0) { 1 }
multi alert ($msg eq "") {}
multi alert ($msg ~~ undef) {}
multimethod add_client ($data, $ID =~ /X\w{4}\d{6}/) {
die "Can't add an X ID";
}
Observe too that in each of these cases, the actual value of the parameter variable is not used within the body of the variant. So you might infer that you could shorten each declaration (and also ensure that the parameter is not "accidentally" used) by declaring the parameter as anonymous:
multi factorial ($ == 0) { 1 }
multi alert ($ == "") {}
multi alert ($ == undef) {}
multimethod add_client ($data, $ =~ /X\w{4}\d{6}/) {
die "Can't add an X ID";
}
Unfortunately that doesn't work, because the constraints are no longer valid Perl code. Or, perhaps we should say: fortunately that doesn't work, because the constraints are no longer valid Perl code, and much less readable.
But the idea of dispensing with the parameter variable and just checking whether an argument matches a particular value is still worthwhile. So Multi::Dispatch allows that too...by permitting any parameter of a variant to be defined by specifying only the value that the corresponding argument it must match (rather than specifying a variable into which that argument must be placed). For example:
multi factorial (0) { 1 } # Argument must == 0
multi alert ("") {} # Argument must eq ""
multi alert (undef) {} # Argument must == undef
# Argument must =~ pattern
multimethod add_client ($data, /X\w{4}\d{6}/) {
die "Can't add an X ID";
}
If a variant is specified with a literal string or number, or an undef
, or a regex at a point where a parameter is expected, then that specification is treated as an anonymous parameter, with the value being treated as if it were a smart-matched :where
constraint.
The effect is very like the "parameter pattern matching" syntax provided in languages such as Raku, Haskell, or Mathematica:
# Perl (with Multi::Dispatch)...
multi factorial (0) { 1 }
multi factorial ($n) { $n * factorial($n-1) }
# Raku...
multi factorial (0) { 1 }
multi factorial ($n) { $n * factorial($n-1) }
-- Haskell...
factorial :: (Integral a) => a -> a
factorial 0 = 1
factorial n = n * factorial (n-1)
(* Mathematica... *)
factorial[0] := 1
factorial[n_] := n * factorial[n-1]
Multiple constraints on a parameter
The three general forms of parameter constraint (prefix types, inline expressions, and postfix :where
attributes) are not mutually exclusive. You can apply any two – or even all three – of them to a single parameter.
For example, the Bernoulli numbers have an interesting property that computing B(n) for integer values of n is generally an expensive and complex operation for even values of n, but is trivial (i.e. always zero) for odd values of n greater than two. You could implement that as:
multi B( Int $N > 2 :where($N % 2) ) { 0 }
multi B( Int $N ) { compute_B_of_even($N) }
You can also combine literal parameter constraints with types constraints and/or :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 :where
attribute.
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 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).
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 if
-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 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:
If the single arrayref argument contains exactly two elements, the first of which is the string
'delete'
, then the first variant will be selected.If the arrayref contains exactly three elements, the first being the string
'insert'
, then the second variant will be selected.If the arrayref contains either two or three elements, the first being the string
'report'
, then the third variant will be selected.If the arrayref contains no elements, then the fourth variant will be selected.
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.
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.
Subparameters may be specified with all the features of regular parameters: named vs anonymous, copied vs aliased, required vs optional vs slurpy (as in the previous example), prefix types, infix expression constraints, literal value constraints (as in the previous example), or postfix :where
constraints. Subparameters can even be specified as nested destructures, if you happen to need to distinguish variants to that degree of structural detail.
For example, if the $ID
subparameter of the first three handle()
variants has to conform to a particular pattern, and the $data
subparameter must a nested hashref (which it would be more convenient to alias, than to copy) and the filehandle argument of the third variant must actually be a filehandle, you could add those constraints to the relevant subparameters:
multi handle( ['delete', $ID :where(/^X\d{6}$/)] ) {...}
# ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑
multi handle( ['insert', \%data, $ID :where(/^X\d{6}$/)] ) {...}
# ↑↑↑↑↑↑ ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑
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] ]
}
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 ($event->[2]
, $client->[17]
) are considerably more error-prone than named look-ups ($event->{ID}
, $client->{overdraft_limit}
)
So it's actually quite unlikely that the 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 (are you 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 key=>
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 (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 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 $opts
parameter by name:
sub mysort ($opts, @data) {
if ($opts->{uniq}) {
@data = uniq @data;
}
if ($opts->{key}) {
if ($opts->{fold}) {
@data = map { [$_, fc $opts->{key}->($_)] } @data;
}
else {
@data = map { [$_, $opts->{key}->($_)] } @data;
}
if ((ArrayRef[Tuple[Num,Any]])->check(\@data)) {
@data = sort { $a->[1] <=> $b->[1] } @data;
}
else {
@data = sort { $a->[1] cmp $b->[1] } @data;
}
@data = map { $_->[0] } @data;
}
elsif ((ArrayRef[Num])->check(\@data)) {
@data = sort { $a <=> $b } @data;
}
elsif ($opts->{fold}) {
@data = sort { fc $a cmp fc $b } @data;
}
else {
@data = sort @data;
}
if ($opts->{rev}) {
return reverse @data;
}
else {
return @data;
}
}
Passing named options in a hashref is certainly a useful API technique, but with multiple dispatch – and especially with hash destructuring – the code can be much cleaner...and entirely declarative:
multi rank (Tuple[Num,Any] @data) { map {$_->[0]} sort {$a->[1] <=> $b->[1]} @data }
multi rank ( ArrayRef @data) { map {$_->[0]} sort {$a->[1] cmp $b->[1]} @data }
multi rank ( Num @data) { sort {$a <=> $b } @data }
multi rank ( @data) { sort @data }
multi mysort ({fold=>1, key=>$k, %opt}, @data)
{ mysort {%opt, key=>sub{fc $k->($_)}}, @data }
multi mysort ({fold=>1, %opt}, @data) { mysort {%opt, key => \&CORE::fc}, @data }
multi mysort ({uniq=>1, %opt}, @data) { mysort \%opt, uniq @data }
multi mysort ({ rev=>1, %opt}, @data) { reverse mysort \%opt, @data }
multi mysort ({ key=>$k, %opt}, @data) { rank map {[$_, $k->($_)]} @data }
multi mysort ({ %opt}, @data) { rank @data }
Notice that each variant only handles one particular option (or combination of options), and hence requires no conditional tests whatsoever within its code. Each variant simply destructures the initial hashref argument to pick out the relevant option(s), and then uses those option(s) to implement each phase of the overall sorting process by:
adding case-folding to the key extractor if both the
'fold'
and'key'
options are specified (variant 1),using case-folding as the key extractor if only the
'fold'
option is specified (variant 2),preprocessing the data with the
uniq()
function if the'uniq'
option is specified (variant 3)postprocessing the sorted data with
reverse()
if the'rev'
option is specified (variant 4)extracting keys and sorting by them in a Schwartzian transform if the
'key'
option is specified (variant 5)doing a simple sort otherwise (variant 6)
Meanwhile the various variants of the rank()
multisub ensure that the correct type of sorting (numeric or stringific, Schwartzian or direct) is applied each time.
DRY hash subparameters
The only real annoyance in using destructuring hashes instead of arrays is the frequent necessity to type each subparameter name twice: once for the key and once for the associated subparameter variable.
Of course, this is not strictly necessary: as long as the key names match the argument's keys, the associated subparameter variables can be named anything you prefer:
multi handle( {cmd => 'insert', ID => $new_ID, data => \%new_record} )
{...}
multi handle( {cmd => 'report', ID => $snorkel, fh => $Albuquerque = *STDOUT} )
{...}
But, more often than not, the key name is also the most sensible name for the subparameter variable, which means the code frequently has to repeat itself:
multi handle( {cmd => 'insert', ID => $ID, data => \%data} )
{...} # ↑↑.....↑↑ ↑↑↑↑......↑↑↑↑
multi handle( {cmd => 'report', ID => $ID, fh => $fh = *STDOUT} )
{...} # ↑↑.....↑↑ ↑↑.....↑↑
To make hash destructures less tedious, less error-prone, and more DRY, the key of any named subparameter can simply be omitted entirely, in which case the missing key is inferred from the name of the associated variable. Thus, the above examples could be rewritten as:
multi handle( {cmd => 'insert', => $ID, => \%data} )
{...} # ↑↑↑↑↑↑ ↑↑↑↑↑↑↑↑↑
multi handle( {cmd => 'report', => $ID, => $fh = *STDOUT} )
{...} # ↑↑↑↑↑↑ ↑↑↑↑↑↑
Note that the "fat commas" (now prefix operators) are still required... to clearly indicate that the missing keys were intentionally omitted and that the module is being explicitly requested to figure them out.
Slurpy hash destructuring
Passing named arguments in a hashref is a convenient technique, but it can also be useful to be able to pass named arguments directly:
EventLoop->add(ID => $ID, cmd => $action, data => $info);
This can, of course, be accomplished with a standard Perl subroutine signature:
sub add ($classname, %args) {...}
...or an Object::Pad method:
method add :common (%args) {...}
...but neither solution actually checks that the correct keys (and only those keys) were passed in, nor does any other kind of type- or constraint-checking.
You could, of course, use a destructuring hashref parameter:
multimethod add :common ( {ID => Num $ID, cmd => Str $c, data => \%d, %etc} )
{...}
...but now the user has to bundle the named arguments in a hashref when they are passed to the method.
To avoid this, Multi::Dispatch allows an implicit destructuring hash to be declared, simply by omitting the curly brackets around an explicit destructuring hash. That is, if the preceding example were rewritten as:
multimethod add :common ( ID => Num $ID, cmd => Str $c, data => \%d, %etc )
{...}
...it's the equivalent of specifying an anonymous slurpy hash parameter:
multimethod add :common (%) {...}
...except that the contents of that anonymous slurpy hash are also destructured into the various subparameters.
That is, the named arguments passed to the multimethod must match the keys of the destructure, and must be present (unless the corresponding subparameter is optional). The values for each key must satisfy any constraints on the subparameter as well. No named argument whose key is not in the destructure specification may be passed...unless the destructure ends with a slurpy hash parameter.
Note that, because this slurpy destructuring syntax is equivalent to a single explicit slurpy parameter, it can only be declared at the end of a parameter list (i.e. after any required or optional unnamed positional parameters). Furthermore, if one or more named parameters are declared, they can only be followed by a(n optional) slurpy hash parameter, not a slurpy array parameter.
Permuted parameter lists
One of the principle uses of multisubs and multimethods is to implement interactions or operations between two or more objects or values. The classic example used throughout the multiply dispatched world is the 70s arcade game: Asteroids. In that game, things can collide with each other, and the result is determined by the nature of the things that are colliding.
That interaction couldl be implemented via multiple dispatch, like so:
multi collide (Asteroid $a, $obj ) { $a->split, $obj->explode }
multi collide ( $obj, Asteroid $a ) { $a->split, $obj->explode }
multi collide (Ship $s->shielded, $obj ) { $s->bounce, $obj->bounce }
multi collide ( $obj, Ship $s->shielded ) { $s->bounce, $obj->bounce }
multi collide (Ship $s->shielded, Missile $m ) { $s->bounce, $m->explode }
multi collide (Missile $m, Ship $s->shielded ) { $s->bounce, $m->explode }
multi collide (Asteroid $a1, Asteroid $a2 ) { $a1->split, $a2->split }
multi collide ( $obj1, $obj2 ) { $obj1->explode, $obj2->explode }
Note that all the collisions between different kinds of objects require two variants...to cover the possibility that the objects will be passed in either order. This is a common occurrence when using multisubs or multimethods to implement interactions, so the module provides a shortcut to simplify specifying multis where the required arguments can be specified in any order: the :permute
attribute. For example, we could create the same set of variants as in the preceding example with just:
multi collide :permute (Asteroid $a, $obj ) { $a->split, $obj->explode }
multi collide :permute (Ship $s->shielded, $obj ) { $s->bounce, $obj->bounce }
multi collide :permute (Ship $s->shielded, Missile $m ) { $s->bounce, $m->explode }
multi collide (Asteroid $a1, Asteroid $a2 ) { $a1->split, $a2->split }
multi collide ( $obj1, $obj2 ) { $obj1->explode, $obj2->explode }
Adding the :permute
attribute to a multisub or multimethod declaration causes that declaration to create multiple variants in which all the required arguments are permuted in every possible way...just like in the earlier example, where all the variants were explicity declared.
For the two required arguments in the preceding example, that means each :permute
declaration produces two variants. If a :permute
multi declaration has three required arguments, then you get six variants. Et cetera.
Each permutation changes the order of the required parameters, but not their names, types, constraints, or any destructuring they may have. Every permutation has exactly the same body too.
Note that the order in which the permutations of a variant are created is not predictable, and should not be relied upon, except that it is guaranteed that the signature of the first permutation generated will always be identical to the permuted variant's actual declaration.
Variant constraints
In addition to specifying constraints on the individual parameters of a variant, you can also place constraints directly on the entire variant itself, by adding a :where
attribute between the variant's name and its parameter list.
For example, you might want to specify two different behaviours for a multimethod, depending on whether a particular field is set:
field $verbose :param;
multimethod report :where({$verbose}) ($msg) {...}
multimethod report ($msg) {...}
Now, when you call:
$obj->report($msg);
...the variant selected depends on whether or not the particular object's $verbose
field is true.
Or you could specify a particular variant to be selected only the first time a given multisub is called:
my $called;
multi report :where({!$called++}) () { say 'first' }
multi report () { say 'not first' }
Note that all of the above examples of variant constraints are block-based. In fact, variant constraints can only be specified as a code block (or a subroutine reference). Unlike parameter constraints, you can't specify a number, string, regex, or type...because a variant itself has no value against which that number, string, regex, or type could be compared.
Context constraints
Yet another use of variant constraints is to select different variants to invoke in different call contexts:
# ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
multi now :where({not defined wantarray}) () { say scalar localtime }
multi now :where({not wantarray}) () { return time }
multi now :where({ wantarray}) () { return scalar localtime }
In fact, this is sufficiently useful that the module provides a shorthand for it:
# ↓↓↓↓↓↓↓↓↓↓↓↓↓↓
multi now :where(VOID) () { say scalar localtime }
multi now :where(SCALAR) () { return time }
multi now :where(LIST) () { return scalar localtime }
You can also specify "not VOID
", "not SCALAR
", and "not LIST
", by prefixing the special keywords with NON
, like so:
# Only print out report in void context...
multi report :where( VOID) () { say $report }
multi report :where(NONVOID) () { return $report }
# Only return raw time() in scalar context, otherwise the pretty version...
multi now :where( SCALAR) () { return time }
multi now :where(NONSCALAR) () { return scalar localtime }
# Only allow this multisub to be called in list context...
multi get_data :where( LIST) () { return @data }
multi get_data :where(NONLIST) () { die "get_data() not in list context" }
:before
variants and variant redispatch
There is one other kind of constraint (or, rather, anti-constraint) that can be applied to an entire variant: it can be promoted to a higher priority in the dispatch process.
Normally, variants are examined from most- to least-specific signatures. That is: variants with more constraints, more destructuring, more required parameters, etc. are tried before variants with fewer of these properties, in a particular ordering described in "How variants are selected for dispatch".
But it is also possible to mark one (or more) variants as being pre-eminent, as coming before all the others in the dispatch process. To do this, you simply mark the variant with a :before
attribute. Every variant specified with a :before
attribute is tried before any variant without that attribute.
The :before
variant is also useful for optimizing the performance of variants whose only real task is to convert one or more arguments to make them compatible with existing variants. For example, suppose you had a multisub whose variants all expected a temperature value, expressed in Celsius:
multi set_temp(Celsius $temp < -273.15 ) {...}
multi set_temp(Celsius $temp < 0 ) {...}
multi set_temp(Celsius $temp > 100 ) {...}
multi set_temp(Celsius $temp ) {...}
...and you now also wanted to handle temperatures specified in Fahrenheit or Kelvin. You could achieve that by adding eight more variants (four each for the two new temperature scales), or you could achieve it by adding just two generic "adaptor" variants:
multi set_temp(Fahrenheit $temp) {
set_temp( Celsius->new(($temp - 32) / 1.8) );
}
multi set_temp(Kelvin $temp) {
set_temp( Kelvin->new($temp - 273.15) );
}
However, this solution requires two entire dispatch processes: first dispatching the original call to select the appropriate Fahrenheit or Kelvin variant, and second to independently dispatch the nested call to select the appropriate Celsius variant.
You could halve the cost of that by placing the two "adaptor" variants at the start of the original dispatch process by marking them with :before
:
multi set_temp :before (Fahrenheit $temp) {...}
multi set_temp :before (Kelvin $temp) {...}
You would then have each new variant resume the dispatch process of the original call, moving on down the list of variants to consider the (now lower precedence) original variants for dispatch instead.
This is known as redispatching a multisub (or multimethod) and is achieved under Multi::Dispatch by calling the special next::variant
function. Like so:
multi set_temp :before (Fahrenheit $temp) {
next::variant( Celsius->new(($temp - 32) / 1.8) );
}
multi set_temp :before (Kelvin $temp) {
next::variant( Celsius->new($temp - 273.15) );
}
Effectively, any call to next::variant()
from within a multisub or multimethod means: "Forget that the original call was dispatched to this variant. Go back to the dispatching process and keep looking for another (less specific) variant to dispatch to instead."
Just like any other Perl subroutine, next::variant
can be called in five ways (each with slightly different effect):
next::variant( @ARGLIST )
-
This form finds the next suitable variant in the original dispatch search and calls it with the given arguments. In other words: it resumes the original dispatch process, but now with a different argument list. When the redispatched call completes, control returns to the current variant, with the return value of the redispatched call being returned as the value of the
next::variant()
call. next::variant @ARGLIST
-
This form does exactly the same thing.
&next::variant(@ARGLIST)
-
This variant does exactly the same thing too. Technically, it also circumvents the multisub's prototype, but this is unimportant, because multisubs don't have prototypes.
&next::variant
-
This form finds the next variant in the original dispatch search and calls it with the original argument list that was passed to the current variant. Once again, after the redispatched call, control (and any return value) returns to the current variant.
goto &next::variant
-
This form finds the next variant in the original dispatch search and calls it with the given arguments. However, control never returns to the current variant; instead the call replaces the current variant on the call stack. (See "goto" in perlfunc)
Debugging via :before
variants
:before
variants also make it possible to inject a "catch-all" variant at the start of the dispatch process. This is particularly useful for debugging. If you have an existing class (say, Value) with multimethods you need to track, you could derive a new class (say, Value::Tracked) and insert a "tracking variant" of each multimethod, like so:
class Value::Tracked :isa(Value) {
# Tracking variant...
multimethod get_value :before (@args) {
say "Calling get_value(@args)...";
my $return_value = &next::variant;
say "get_value(@args) returned: $return_value";
return $return_value;
}
...
}
As usual, the Value::Tracked class will inherit all the get_value()
variants that were defined in the Value class. Then the derived class adds a generic get_value(@args)
variant. Normally that variant, with its highly non-specific single slurpy parameter, would be considered very late in the dispatch process and so would likely never be called at all.
However, because the variant is specified with a :before
attribute, it is promoted up the dispatch list, ahead of every variant without such an attribute. Therefore, despite being very generic, the new get_value()
variant will be selected first, and is able to report the call and its result.
Note that it's critically important in situations like these to preserve the actual inherited behaviour(s) of the multimethod. However, the :before
variant doesn't actually attempt to replicate the behaviour of the existing inherited variants. Instead it simply redispatches the call directly to those other variants, using the next::variant
feature.
In fact, in this instance, it's essential that it use next::variant
, rather than recursively calling get_value()
explicitly:
multimethod get_value :before (@arglist) {
say "Calling get_value(@arglist)...";
my $return_value = $self->get_value(@arglist); # <–– DON'T DO THIS!!!
say "get_value(@arglist) returned: $return_value";
return $return_value;
}
Because the parameter list of the :before
variant is so high-priority and also so general, that variant will always be selected at the start of any call, so if that variant calls the same multimethod recursively, the nested call will start back at the :before
variant again, select it again, and recurse again...forever.
But if the :before
variant redispatches using next::variant
, the multimethod will step back into to the previous dispatch process (which had already selected the :before
variant)...and will simply continue onwards to find the next most suitable variant instead.
Whenever you encounter a situation where a recursive call within a variant seems needed, consider whether you could use next::variant
instead. Most of the time, you probably can, and will improve the robustness and efficiency of you code if you do.
For example, in "Hash destructuring" we saw how a powerful sorting function could be implemented cleanly using multisubs:
multi mysort ({fold=>1, key=>$k, %opt}, @data)
{ mysort {%opt, key=>sub{fc $k->($_)}}, @data }
multi mysort ({fold=>1, %opt}, @data) { mysort {%opt, key => \&CORE::fc}, @data }
multi mysort ({uniq=>1, %opt}, @data) { mysort \%opt, uniq @data }
multi mysort ({ rev=>1, %opt}, @data) { reverse mysort \%opt, @data }
multi mysort ({ key=>$k, %opt}, @data) { rank map {[$_, $k->($_)]} @data }
multi mysort ({ %opt}, @data) { rank @data }
But this code can be significantly optimized by noting that every recursive call to mysort()
within any of the variants can actually only select one of the variants following it...because each variant effectively removes the named option(s) that caused it to be selected from the %opt
hash before recursing.
This strict ordering of the variants means you can eliminate all the expensive recursion by replacing each nested call to mysort()
with a next::variant
redispatch instead:
multi mysort ({fold=>1, key=>$k, %opt}, @data)
{ next::variant {%opt, key=>sub{fc $k->($_)}}, @data }
multi mysort ({fold=>1, %opt}, @data) { next::variant {%opt, key => \&CORE::fc}, @data }
multi mysort ({uniq=>1, %opt}, @data) { next::variant \%opt, uniq @data }
multi mysort ({ rev=>1, %opt}, @data) { reverse next::variant \%opt, @data }
multi mysort ({ key=>$k, %opt}, @data) { rank map {[$_, $k->($_)]} @data }
multi mysort ({ %opt}, @data) { rank @data }
Each call to mysort()
now only invokes a single subroutine, which works its way progressively through the appropriate variants, without ever reconsidering or re-invoking variants that have already been invoked (or rejected).
Note, however, that in general next::variant
doesn't mean: "Go to the next variant declared below this one". It means: "Go to next variant in the intrinsic most-specific-first order in which variants are always considered from dispatch". The following section explains that intrinsic order more fully.
How variants are selected for dispatch
The goal of defining a multisub or multimethod is to provide a range of distinct and specific behaviours that are invoked in response to distinct and specific lists of arguments...without having to explicitly code endless tests within a subroutine to determine which particular behaviour should be selected for a given argument list.
The goal is to have the most appropriate and most relevant behaviour (i.e. variant) automatically selected each time a given multisub or multimethod is called, and to have that selection made as quickly and efficiently as possible.
Unfortunately, "appropriate" and "relevant" are extremely nebulous terms, which have been interpreted and realized differently in almost every programming language or language-extension module that supports multiple dispatch.
This particular language-extension module aims to provide a reasonable and predictable interpretation of appropriate and relevant; one that incorporates the best features of many of those other implementations, while imposing as little runtime overhead as possible on individual multisub and multimethod calls.
To this end, Multi::Dispatch defines a static strict total ordering on the variants of a given multisub or multimethod, and then sorts those variants into that order at compile-time. The ordering is static because it can be determined entirely from compile-time information; the ordering is strict because the precedence of any two variants is never ambiguous, undefined, or "equal"; and the ordering is total because every possible pair of variants can be strictly compared.
Then, when it is actually invoked with an argument list, each multisub or multimethod simply steps through its ordered list of variants, considering each in turn, until it finds one that can successfully handle that argument list (i.e. the first variant whose arity matches the number of arguments and whose parameter constraints are satisfied by each argument).
The first such successful variant is then immediately called, without considering any others later in the variant list. Because later variants are, ipso facto, inherently less appropriate or relevant.
To ensure that the earlier variants in the list are indeed the most appropriate and relevant ones, the variants are sorted (at compile-time) according to the following nine successive criteria (yeah, I know, that sounds horribly complicated and not at all "intuitive", but it's actually as easy as A-B-C...D-E-F-G-H-I):
- Arity
-
If a multisub or multimethod is called with N arguments, only its variants that define sufficient parameters to contain all N arguments are ever considered for dispatch.
That is, the only variants that are considered are those that define at least N scalar parameters, plus those that define fewer than N scalar parameters plus a slurpy parameter.
Similarly, only those variants that define no more than N required parameters are considered.
For example:
foo(1,2); multi foo ($i) {...} # Excluded (too few params) multi foo ($i, $j) {...} # Considered (correct number of params) multi foo ($i, $j, $k) {...} # Excluded (too many required params) multi foo ($i, $j, $k = 0) {...} # Considered (can accept 2 args) multi foo ($i, @etc) {...} # Considered (can accept 2 args) multi foo ($i, $j, @etc) {...} # Considered (can accept 2 args) multi foo ($i, %etc) {...} # Excluded (cannot accept 2 args) multi foo (%etc) {...} # Considered (can accept 2 args)
- Beforeness
-
If two or more variants all have the correct arity, the variants with a
:before
attribute will be considered for dispatch before any variant without a:before
. - Constraint
-
If two or more variants all have the same
:before
status, the variant with a greatest total number of constraints will be considered for dispatch before any variants with fewer constraints. For example:multi foo ($x) {...} # Tried third (no constraints) multi foo (Int $x < 10) {...} # Tried first (two constraints) multi foo ($x :where(0)) {...} # Tried second (one constraint)
If two or more variants all have a type constraint on the same parameter, the variant with the more restrictively typed parameter will be considered for dispatch first. For example:
multi foo (Value $x) {...} # Tried third multi foo (Int $x) {...} # Tried first multi foo (Num $x) {...} # Tried second
...because
Int
is a tighter constraint on argument values thanNum
, which in turn is a tighter constraint thanValue
.Likewise, type constraints that specify class membership are ordered most-derived-first. For example:
multi foo (Animal:: $x) {...} # Tried third multi foo (Animal::Primate $x) {...} # Tried first multi foo (Animal::Mammal $x) {...} # Tried second
...because
Animal::Primate
isaAnimal::Mammal
andAnimal::Mammal
isaAnimal
. - Destructuring
-
If two or more variants have the same degree of constraint, the variant with a greatest total number of destructuring parameters will be considered for dispatch before any variants with fewer destructures. For example:
multi foo ($x, {=>$name}) {...} # Tried second (one destructure) multi foo ([$x0], {=>$name}) {...} # Tried first (two destructures) multi foo ($x, $y ) {...} # Tried third (no destructures)
- Essentials
-
If two or more variants define the same number of destructuring parameters, the variant with a greatest number of required (i.e. essential) parameters will be considered for dispatch before any variants with fewer required parameters.
In other words, a variant with a given number of required parameters will be considered for dispatch before a variant with the same number of parameters where some of them are optional.
For example:
multi foo ($x, $y = 1) {...} # Tried second (one required param) multi foo ($x, $y ) {...} # Tried first (two required params) multi foo ($x = 0, $y = 1) {...} # Tried third (no required params)
- Facultativity
-
(Facultativity: n. The state of being optional)
If two or more variants have the same number of required parameters, the variant with the fewest optional parameters will be considered for dispatch before any variant with a greater number of optional parameters (because a variant that has fewer optional parameters can be considered to be more specific in its requirements).
For example:
multi foo ($x, $y = 1 ) {...} # Tried second (one optional param) multi foo ($x ) {...} # Tried first (no optional params) multi foo ($x, $y = 1, $z = 2) {...} # Tried third (two optional params)
- Greed
-
If two or more variants have the same number of optional parameters, the variants without slurpy parameters will be considered for dispatch before any variants with slurpy parameters (again, because being able to accept an argument list of any length is inherently less specific than being restricted to a fixed number of arguments).
Alternatively, you can think of a single slurpy parameter as being equivalent to an infinite number of optional parameters, so variants with a slurpy array or hash are always considered after variants without a slurpy, regardless of the actual number of optional scalar parameters each has.
For example:
multi foo ($x, @etc ) {...} # Tried second (one slurpy param) multi foo ($x ) {...} # Tried first (no slurpy param)
- Heredity
-
If two or more variants of a multimethod have the same number of slurpy parameters, then any multimethod variant that was defined in a derived class will be considered for dispatch ahead of any variant defined in one of its base classes.
In other words, the complete set of variants defined directly in a given class are always considered ahead of any variants that have been inherited from some base class in its hierarchy.
Note that this sorting criterion does not apply to the variants of multisubs, even if they are defined within related classes (because multisubs are not methods, so multisub variants are never inherited).
- Inception
-
If two or more variants cannot be distinguished by any of the preceding criteria, then they are considered for dispatch in the order they came into being (i.e. the order in which they were declared).
Note that every possible set of variants is strictly linearly ordered by this final criterion, so no further sorting criteria are required.
Debugging multiply dispatched calls
When you have numerous variants of a multisub or multimethod all vying for the same call dispatch, it may not always be obvious why one particular variant was selected for a given argument list. Especially with multimethods, where inherited variants may sometimes be chosen in preference to in-class variants.
Moreover, sometimes no variant at all can be selected, and it won't always be obvious why. To assist in debugging unexpected or unsuccessful dispatches, Multi::Dispatch offers three flags that provide more information.
Normally, when a dispatch fails, the module throws an exception that succinctly reports that failure:
No suitable variant for call to multi handle()
with arguments: ({ cmd => "del", data => undef, key => "acct1" })
at demo.pl line 18
However, if the module was loaded with the -verbose
flag specified:
use Multi::Dispatch -verbose;
...then a more detailed explanation of which variants were considered, and why they were rejected, is printed to STDERR:
No suitable variant for call to multi handle()
with arguments: ({ cmd => "del", data => undef, key => "acct1" })
at demo.pl line 18
B1: main::handle (\@args)
defined at demo.pl line 14
--> 1st argument was not a array reference,
so it could not be aliased to parameter \@args
C2: main::handle (ARRAY $argref != undef)
defined at demo.pl line 13
--> 1st argument did not satisfy constraint on
parameter $argref: ARRAY and $argref != undef
D1: main::handle ({cmd=>'set', key=>$key, data=>$data})
defined at demo.pl line 10
--> Incorrect number of entries in hashref argument 0:
expected exactly 3 entries
D1: main::handle ({cmd=>'del', key=>$key})
defined at demo.pl line 11
--> Required key (->{'key'}) not found in hashref argument $ARG[0]
E3: main::handle ($x, $y, $z)
defined at demo.pl line 16
--> SKIPPED: need at least 3 args but found only 1
F2: main::handle (\@args = [], $opt = undef)
defined at demo.pl line 15
--> 1st argument was not a array reference,
so it could not be aliased to parameter \@args
The two-character prefix on each report indicates why the variant was considered at that point in the overall dispatch process. The letters correspond to the A-to-I precedence categories of variants (see "How variants are selected for dispatch"), and the number indicates the degree of precedence within each category.
Hence, in the preceding example, the B1 prefix indicates that the first variant considered was a :before
variant, which is why it was tried before the variant with 2 constraints (C2), which in turn was tried ahead of the two variants with a single destructuring parameter (D1), then the variant with exactly three required parameters (E3), and finally the "fuzzy" variant with two optional parameters (F2).
The -verbose
option only affects the reporting of failed dispatches; successful dispatches remain silent. But if a "successful" dispatch doesn't do what you wanted, that's a bug of some kind (even if it's just a bug in your understanding), so the module also allows you to investigate how any particular successful dispatch decided which variant to actually call.
If the module is loaded with the -debug
flag:
use Multi::Dispatch -debug;
...then the entire dispatch process of every multisub or multimethod call (successful or not) is reported to STDERR.
For example, a successful dispatch of the handle()
multisub might report:
Dispatching call to handle({ cmd => "del", key => "acct2" })
at demo.pl line 19
B1: main::handle (\@args)
defined at demo.pl line 14
--> 1st argument was not a array reference,
so it could not be aliased to parameter \@args
C2: main::handle (ARRAY $argref != undef )
defined at demo.pl line 13
--> 1st argument did not satisfy constraint on parameter $argref:
also still and $argref != undef
D1: main::handle ({cmd=>'set', key=>$key, data=>$data})
defined at demo.pl line 10
--> Incorrect number of entries in hashref argument 0:
expected exactly 3 entries
D1: main::handle ({cmd=>'del', key=>$key })
defined at demo.pl line 11
==> SUCCEEDED
...indicating that three higher-precedence variants were considered and rejected, before the fourth variant was selected.
Note that the report format under -debug
is exactly the same as under -verbose
, and that failed dispatches are also still reported in full.
If you are only interested in checking the order in which variants would be dispatched, load the module with the -annotate
option. This causes the module to output (at compile-time, and to STDERR) a list of warnings indicating the order in which each variant would be tested during dispatch. For example, under the -annotate
flag, you might get:
1st (B1) at demo/demo_dd.pl line 12
9th (E2) at demo/demo_dd.pl line 15
5th (C1) at demo/demo_dd.pl line 16
6th (C1) at demo/demo_dd.pl line 17
10th (E1) at demo/demo_dd.pl line 20
7th (C1) at demo/demo_dd.pl line 21
3rd (C1) at demo/demo_dd.pl line 22
4th (C1) at demo/demo_dd.pl line 25
2nd (C2) at demo/demo_dd.pl line 26
8th (C1) at demo/demo_dd.pl line 29
This indicates the order of the ten variants of the dd
multisub within the demo/demo_dd.pl file. A suitable editor or environment (such as Vim with the ALE plugin) would be able to actually annotate each of these source lines, indicating the ordering right at the variant declarations. For example:
# Create a mini Data::Dumper clone that outputs in void context...
multi dd :before :where(VOID) (@data) { say &next::variant } # 1st (B1)
# Format pairs and array/hash references...
multi dd ($k, $v) { dd($k) . ' => ' . dd($v) } # 9th (E2)
multi dd (\@data) { '[' . join(', ', map {dd($_)} @data) . ']' } # 5th (C1)
multi dd (\%data) { '{' . join(', ', map {dd($_, $data{$_})} keys %data) . '}' } # 6th (C1)
# Format strings, numbers, regexen...
multi dd ($data) { '"' . quotemeta($data) . '"' } # 10th (E1)
multi dd ($data :where(\&looks_like_number)) { $data } # 7th (C1)
multi dd ($data :where(Regexp)) { 'qr{' . $data . '}' } # 3rd (C1)
# Format objects...
multi dd (Object $data) { '<' .ref($data).' object>' } # 4th (C1)
multi dd (Object $data -> can('dd')) { $data->dd(); } # 2nd (C2)
# Format typeglobs...
multi dd (GLOB $data) { "" . *$data } # 8th (C1)
Note that the ordering also reports why the variant is in that position in the dispatch sequence, by reporting its A-to-I precedence category and score.
All three of these debugging flags are lexical in scope. That is, they only apply to multisubs and multimethods defined in the lexical scope where they were specified.
Object::Pad integration
When creating multimethods, Multi::Dispatch will detect if the Object::Pad module is active at the point a multimethod variant is declared. If it is, the overall multimethod dispatcher, as well as every individual variant of the multimethod, will be implemented as an Object::Pad method
. This means that Multi::Dispatch multimethods have access to any fields or other methods declared in an Object::Pad-based class.
In all other respects, multimethods are the same as in non-Object::Pad classes, especially with respect to inheritance.
Eventually, it is hoped that Multi::Dispatch will be similarly aware of the new builtin class mechanism currently being added to Perl.
Exporting a multisub to another module
Internally, a multisub is implemented as a regular subroutine, so it may be exported from a module in the same way as any other subroutine (e.g. via one of the many Exporter
modules, or by direct typeglob assignment inside an import()
method)
For example:
package FooSource;
use Multi::Dispatch;
multi foo ($x) { say 1 }
multi foo ($x, $y) { say 2 }
use Exporter 'import';
our @EXPORT = 'foo';
However, if your code first defines one or more variants of a multisub and only then imports the same multisub from a module:
use Multi::Dispatch;
multi foo ($x, $y, $z) { say 3 } # First define a multisub
use FooSource; # Then import a multisub of the same name
...then the imported foo()
multisub will overwrite the locally defined foo()
multisub (or, technically, the imported sub-implementing-your-foo()
-multisub will overwrite the locally defined sub-implementing-your-foo()
-multisub). When this happens you will get a "Subroutine main::foo redefined at..." warning.
This outcome is inevitable, because Multi::Dispatch
has no control over the behaviour of the module's export mechanism, so it can't intercept and correct the subroutine definition.
To avoid this issue, import FooSource
's foo()
multisub before declaring any extra local variants:
use Multi::Dispatch;
use FooSource; # First import the multisub
multi foo ($x, $y, $z) { say 3 } # Then define another variant of the same name
This works because the following multi
declaration can detect the previously imported multisub and will avoid redeclaring the handler sub.
For other approaches, which do allow you to import multisub variants after having declared local variants, see the following two sections.
Importing multisub variants from another module
Unlike multimethods, a given multisub normally consists of only those variants that have been declared in the same package. Variants with the same name, but which were declared in a different package are never considered during the dispatch process, even when a particular multisub is called outside its own package.
However, it is also possible to pre-import multisub variants from other namespaces and still have them included in the list of dispatch candidates for a particular multisub. This is accomplished via the :from
attribute.
If you declare a multisub without a code block, but with a :from
attribute, that declaration will import all the variants from the nominated package into the corresponding multisub in the current package. For example, to add all the variants of the Logger::debug()
multisub to the candidate list of MyModule::debug()
multisub:
package MyModule;
multi debug :from(Logger);
There is no requirement that a local variant of the debug()
multisub has already be defined when the :from
declaration is made, so this mechanism also provides a way of simply importing a multisub from another package, without extending it in any way (even if that module doesn't explicitly export the multisub).
You can import other variants from as many other packages as you wish, by adding further :from
declarations within the current package, as well as adding extra variants explicitly (either before or after the imports):
multi debug ($level, $msg) { warn $msg if $level >= $THRESHOLD }
multi debug :from( Debugger::Remote );
multi debug :from( Console::Reporter::DBX );
multi debug :from( Term::Debug );
multi debug (@msg) { warn @msg }
In the preceding examples, the imported variants are imported from multisubs of the same name (i.e. debug
) from the specified packages. However, you can also import variants from a differently named multisub, by specifying the different name of that multisub explicitly (with a leading &
to indicate that it's a multisub name, not a package name). Like so:
multi debug :from( &Debugger::Remote::rdebug );
multi debug :from( &Console::Reporter::DBX::dbx );
multi debug :from( &Term::Debug::show );
Note that all these variants of the rdebug()
, dbx()
, and show()
multimethods are imported as local variants of the debug()
multisub, despite their different original names.
Each :from
declaration will attempt to require
the specified package, but will not use
it. If a particular module (for example: Console::Reporter::DBX
) needs to be loaded with specific arguments in order to work correctly, then you must do that explicitly, prior to the :from
declaration:
use Console::Reporter::DBX (output => \*STDERR, level => 'warn');
multi debug :from( &Console::Reporter::DBX::dbx );
Exporting multisub variants to another module
In addition to importing multisub variants from a module, you can also export a multisub and its variants from a module, by using another declarative syntax within the import()
of the module. Like so:
sub import {
...
multi dd :export; # Export this module's dd multis at this point
...
}
# Then later...
multi dd (ARRAY $a) {...}
multi dd (HASH $h) {...}
multi dd (Regexp $r) {...}
multi dd (Num $n) {...}
# et cetera...
That is, if you declare a multisub with a name and the :export
attribute, but with no code block, then all the variants of correspondingly named multisub from the current module will be exported to the caller's namespace at that point.
Note that this kind of declarative :export
request must be attached to an "empty" multi declaration inside the body of the import()
subroutine, not on any of the actual variants declared within the module.
Declarative exports of the type do not suffer from the overriding issues and order limitations that were discussed in "Exporting a multisub to another module".
DIAGNOSTICS
Isolated variant of multi(method) <NAME>
-
The module detected two variant declarations belonging to the same multisub or multimethod, separated by a non-trivial amount of unrelated code.
Multisubs and multimethods distribute their behaviour across multiple variants. If those variants are spread at random through the code, they are often much harder to understand, to predict, or to debug.
To disable this warning, either collect all the variants together in one location within the package, or else turn off the warning explicitly:
no warnings 'Multi::Dispatch::noncontiguous';
(And, yes that's deliberately ponderous...in order to discourage its use. ;-)
A slurpy parameter (<NAME>) may not have a default value: <DEFAULT>
-
Slurpy parameters are inherently optional, and indicate the absence of corresponding arguments simply by remaining empty. So it is not necessary – or possible – to specify a default value for them. Just as with regular Perl slurpy parameters.
(Providing a default mechanism for slurpies was considered and even prototyped, but in testing it was found that that permitting a list of default values to be specified for a slurpy within the context of the surrounding parameter list led to frequent syntax errors because of list flattening. Note, however, that if a more robust solution can be found, this prohibition may eventually be revoked.)
Meanwhile, it's easy enough to test for an empty slurpy and to populate it with default values if necessary:
multi mysort(@list) { @list = @DEFAULTS if !@list; ... }
Can't redispatch via next::variant
-
Your code requested some form of redispatch using the
next::variant
mechanism, but that code was not within a multisub or multimethod, which is the only placenext::variant
is available (or makes sense).Either remove the call to
next::variant
, or convert it to something that will work correctly: perhapsnext::method
, or a named subroutine or method call. Alternatively, convert your subroutine/method into a multisub/multimethod. Can't specify a required parameter (<NAME>) after an optional or slurpy parameter
-
Required parameters must be defined at the start of a parameter list, before any optional or slurpy parameter.
Move the parameter to the left of the first such optional. Alternatively, was this supposed to be an optional parameter, but you forget the
=
and default value? Can't parameterize a Perl class (<NAME>) in a type constraint
-
Only Type::Tiny named types can be parameterized (e.g.
ArrayRef[Int]
) as part of of type constraint. You attempted to append square brackets to something that was not recognized as a Type::Tiny type.Did you misspell the type name?
Can't specify return type (<NAME>) on code parameter <NAME>
-
There is no way to detect the return type of a Perl subroutine. Code parameters are always bound to subroutines, so there is no point in trying to specify the return type of a code parameter, as that return type could never be tested.
Simply remove the type specifier associated with the parameter.
Could not load type <TYPENAME>
-
You specified a Type::Tiny-style typename as a parameter constraint, but the specified type could not be loaded.
Did you forget to
use Types::Standard
, or some other type-defining module? Or did you forget to specify which types the module should export? Types::Standard and its ilk don't export any types by default. So perhaps you need:use Types::Standard ':all'
Alternatively, were you attempting to use a simple classname that doesn't have a
::
in it? If so, add a::
to either the beginning or end of the classname. <TYPENAME> constraint is ambiguous (did you mean <TYPENAME>:: instead?)
-
You specified a valid Type::Tiny typename as a parameter constraint, but the type's name is also the name of a defined class, which means it's possible you might have wanted the class instead of the type.
If you wanted the class, add a
::
before or after the constraint name.If you wanted the type, but don't want the warning, either rename the class or else specify
no warnings 'ambiguous';
before the declaration. Default value for parameter <NAME> cannot include a 'return' statement
-
Although
return
expressions are permitted in regular Perl subroutine signatures, they are not permitted in multisubs and multimethods, because they interfere with the internal variant-selection process.Instead of a short-circuited return from within the parameter list:
method name ($newname = return $name) { $name = $newname; }
...just specify the appropriate variants directly:
multimethod name () { return $name; } multimethod name ($newname) { $name = $newname; }
Incomprehensible constraint: <CONSTRAINT>
-
You specified a
:where(...)
constraint that could not be recognized as a value, a regular expression, a code block, a subroutine reference, or a typename.Did you misspell the constraint?
Invalid declaration of multi(method)
-
The module detected that something wasn't right with your declaration. It will do its best to indicate what and where the unacceptable syntax occurred.
Invalid multi(method) constraint: <CONSTRAINT>
-
A
:where(...)
constraint that's applied to an entire multisub or multimethod can only be a code block, a subroutine reference, or a context specifier. You can't use a literal value or regex or typename or classname as a constraint for an entire multisub or multimethod variant, because the variant as a whole has no value against which that literal, regex, typename, or classname could be compared.Reconsider what you meant by comparing the entire variant against a literal value, and reformulate that goal as a block of code instead.
Can't declare a multi and a multimethod of the same name in a single package
-
Like regular Perl subs and methods, multisubs and multimethods are both just subroutine references stored in the local symbol table. This means you can't have both a multisub and a multimethod of the same name in the same package, because they would both require a dispatching subroutine of that name...and there can be only one subroutine of a given name per package.
To avoid this compile-time error, change the name of either the multisub or the multimethod to something unique in the namespace.
Subroutine <NAME> redefined as multi(method) <NAME>
-
You already had a regular Perl subroutine of the specified name defined within the current package (or possibly imported into the current package from some other module). This warning is reminding you that defining a multisub or a multimethod of that same name causes the pre-existing regular subroutine to be replaced by the dispatcher subroutine for the multisub/multimethod. You probably didn't want that.
To avoid this compile-time warning, change the name of either the pre-existing regular subroutine, or of the new multisub/multimethod.
Or, if you really do want to replace the pre-existing subroutine with a multisub or multimethod of the name name, you can silence the warning with a:
no warnings 'redefine';
Multi(method) <NAME> [imported from <PACKAGE>] redefined as multi(method) <NAME>
-
There was already a multisub or multimethod of the specified name installed in the current package, which must have been imported from some other package using the standard Perl module export mechanism. Variants defined in the current package won't be added to the variants of a regularly imported multisub or multimethod. Instead, they will replace that imported multisub or multimethod entirely. This compile-time warning is reminding you about that.
If you did intend to replace the imported multisub or multimethod, you can silence this warning with a:
no warnings 'redefine';
If you intended to extend the imported multisub or multimethod, you'll have to import it with the module's own multiple-dispatch-aware import mechanism instead. (See "Importing variants from another module").
If you didn't intend to either replace or extend the imported multisub or multimethod, maybe just choose another name for the one you are now defining.
Can't import variants for multimethod <NAME> via a :from attribute
-
The
:from
import mechanism (see: "Importing variants from another module") will only import variants for a multisub, not a multimethod.If you need extra multimethod variants from some other class or role, inherit from the class, or compose in the role.
No suitable variant for call to multi(method) <NAME>
-
You called the named multi(method) with a particular list of arguments, but none of the multi(method)'s variants could be bound successfully to those arguments.
This probably indicates that you're missing at least one variant, which could handle that particular set of arguments. Or else, the set of arguments itself was a mistake.
To get more detail on why none of the variants matched, add the
-verbose
option to youruse Multi::Dispatch
statement. See "Debugging multiply dispatched calls". Slurpy parameter <NAME> can't be given a string, an undef, or a regex as a constraint
-
Slurpy parameters can be assigned multiple arguments, so the meaning of smartmatching a single-valued
:where
constraint against the contents of a slurpy array or hash is inherently ambiguous. It could mean any of the following:Match if every element in the slurpy matches the single value;
Match if at least one element in the slurpy matches the single value;
Match the scalar value (i.e. the count) of the slurpy against the single value;
Match the string-concatenation of the slurpy against the single value.
If you want the literal value or regex constraint to be tested for every element, use the following alternatives (each of which makes use of the
all()
function from List::MoreUtils):# Instead of... Use... @slurpy :where(42) ––––> @slurpy :where({all {$_ == 42 } @slurpy}) @slurpy :where('string') ––––> @slurpy :where({all {$_ eq 'string'} @slurpy}) @slurpy :where(undef) ––––> @slurpy :where({all {!defined} @slurpy}) @slurpy :where(/regexp?/) ––––> @slurpy :where({all {/regexp?/} @slurpy})
Or you could specify your constraint as a specialized Types::Standard type (because prefix types are automatically applied to every value in a slurpy):
# Instead of... Use... @slurpy :where('string') ––––> Enum['string'] @slurpy @slurpy :where(undef) ––––> Undef @slurpy @slurpy :where(/regexp?/) ––––> StrMatch[qr/regexp?/] @slurpy
If you want one of the other three possible interpretations, just write the appropriate
:where
block instead:# Instead of... Use one of these... @slurpy :where(42) ––––> @slurpy :where({ grep {$_ == 42} @slurpy}) ––––> @slurpy :where({ @slurpy == 42} ) ––––> @slurpy :where({"@slurpy" eq 42} )
The multi <NAME> can't be given a :common attribute
-
The
:common
attribute is only meaningful for multimethods (it gives them an automatic$class
variable and makes them callable on classes as well as on individual objects).Either change the
multi
keyword tomultimethod
, or else delete the attribute. Internal error <DESCRIPTION>
-
Well that certainly shouldn't have happened!
Congratulations, you found a lurking bug in the module itself. Please consider reporting it, so it can be fixed.
CONFIGURATION AND ENVIRONMENT
None. The module requires no configuration files or environment variables.
DEPENDENCIES
This module only works under Perl v5.22 and later.
The module requires the CPAN modules Data::Dump, Keyword::Simple, Algorithm::FastPermute, and PPR.
If Type::Tiny types are used within the signature of a multi or multimethod, then the actual module (Types::Standard, Types::Common::Numeric, Types::Common::String, etc.) that provides the types must also be loaded within the scope of that declaration.
The Object::Pad module is not required, but Multi::Dispatch will make use of it, provided it has been loaded at the point where a particular multimethod is declared.
INCOMPATIBILITIES
None reported.
However, this module parses multi and multimethod code blocks using PPR, so if you are using other modules that add keywords to Perl, those keywords are unlikely to be usable within such code blocks.
The one exception to this general rule is the Object::Pad module, whose extended keyword syntax Multi::Dispatch knows about and handles correctly.
LIMITATIONS
Multimethod variants cannot at present be composed in from an Object::Pad role
. There is currently no satisfactory workaround for this.
BUGS
No bugs have been reported.
Please report any bugs or feature requests to bug-multi-dispatch@rt.cpan.org
, or through the web interface at http://rt.cpan.org.
ACKNOWLEDGEMENTS
My sincere thanks to Curtis "Ovid" Poe, for asking a simple question that, at the time, had no good answer. I hope you find this answer at least reasonable, Ovid.
My deepest appreciation to:
Paul Evans, for his extraordinary work on Object::Pad and the new Perl OO mechanism
Toby Inkster, for the remarkable Type::Tiny ecosystem
Lukas Mai, for the invaluable Keyword::Simple module
My profound gratitude to Larry Wall, and everyone else in the Raku development team, for creating something so wonderful...and so eminently worth stealing from. :-)
AUTHOR
Damian Conway <DCONWAY@CPAN.org>
LICENCE AND COPYRIGHT
Copyright (c) 2022, Damian Conway <DCONWAY@CPAN.org>
. All rights reserved.
This module is free software; you can redistribute it and/or modify it under the same terms as Perl itself. See perlartistic.
DISCLAIMER OF WARRANTY
BECAUSE THIS SOFTWARE IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY FOR THE SOFTWARE, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE SOFTWARE "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE SOFTWARE IS WITH YOU. SHOULD THE SOFTWARE PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR, OR CORRECTION.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTRIBUTE THE SOFTWARE AS PERMITTED BY THE ABOVE LICENCE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE SOFTWARE (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE SOFTWARE TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.