NAME
Sub::MultiMethod - yet another implementation of multimethods
SYNOPSIS
How to generate JSON (albeit with very naive string quoting) using multimethods:
use v5.20;
use strict;
use warnings;
use experimental 'signatures';
package My::JSON {
use Moo;
use Sub::MultiMethod qw(multimethod);
use Types::Standard -types;
multimethod stringify => (
positional => [ Undef ],
code => sub ( $self, $undef ) {
return 'null';
},
);
multimethod stringify => (
positional => [ ScalarRef[Bool] ],
code => sub ( $self, $bool ) {
return $$bool ? 'true' : 'false';
},
);
multimethod stringify => (
alias => "stringify_str",
positional => [ Str ],
code => sub ( $self, $str ) {
return sprintf( q<"%s">, quotemeta($str) );
},
);
multimethod stringify => (
positional => [ Num ],
code => sub ( $self, $n ) {
return $n;
},
);
multimethod stringify => (
positional => [ ArrayRef ],
code => sub ( $self, $arr ) {
return sprintf(
q<[%s]>,
join( q<,>, map( $self->stringify($_), @$arr ) )
);
},
);
multimethod stringify => (
positional => [ HashRef ],
code => sub ( $self, $hash ) {
return sprintf(
q<{%s}>,
join(
q<,>,
map sprintf(
q<%s:%s>,
$self->stringify_str($_),
$self->stringify( $hash->{$_} )
), sort keys %$hash,
)
);
},
);
}
my $json = My::JSON->new;
say $json->stringify( {
foo => 123,
bar => [ 1, 2, 3 ],
baz => \1,
quux => { xyzzy => 666 },
} );
While this example requires Perl 5.20+, Sub::MultiMethod is tested and works on Perl 5.8.1 and above.
DESCRIPTION
Sub::MultiMethod focusses on implementing the dispatching of multimethods well and is less concerned with providing a nice syntax for setting them up. That said, the syntax provided is inspired by Moose's has
keyword and hopefully not entirely horrible.
Sub::MultiMethod has much smarter dispatching than Kavorka, but the tradeoff is that this is a little slower. Overall, for the JSON example in the SYNOPSIS, Kavorka is about twice as fast. (But with Kavorka, it would quote the numbers in the output because numbers are a type of string, and that was declared first!)
Functions
Sub::MultiMethod exports nothing by default. You can import the functions you want by listing them in the use
statement:
use Sub::MultiMethod "multimethod";
You can rename functions:
use Sub::MultiMethod "multimethod" => { -as => "mm" };
You can import everything using -all
:
use Sub::MultiMethod -all;
Sub::MultiMethod also offers an API for setting up multimethods for a class, in which case, you don't need to import anything.
multimethod $name => %spec
The specification supports the same options as Type::Params v2 to specify a signature for the method, plus a few Sub::MultiMethod-specific options. Any options not included in the list below are passed through to Type::Params. (The options goto_next
, on_die
, message
, and want_*
are not supported.)
code
(CodeRef)-
Required.
The sub to dispatch to. It will receive parameters in
@_
as you would expect, but these parameters have been passed through the signature already, so will have had defaults and coercions applied.An example for positional parameters:
code => sub ( $self, $prefix, $match, $output ) { print { $output } $prefix; ...; },
An example for named parameters:
code => sub ( $self, $arg ) { print { $arg->output } $arg->prefix; ...; },
Note that
$arg
is an object with methods for each named parameter.Corresponding examples for older versions of Perl without signature support.
code => sub { my ( $self, $prefix, $match, $output ) = @_; print { $output } $prefix; ...; },
And:
code => sub { my ( $self, $arg ) = @_; print { $arg->output } $arg->prefix; ...; },
signature
(CodeRef)-
Optional.
If
signature
is set, then Sub::MultiMethod won't use Type::Params to build a signature for this multimethod candidate. It will treat the coderef as an already-built signature.A coderef signature is expected to take
@_
, throw an exception if the arguments cannot be handled, and return@_
(possibly after some manipulation). alias
(Str|ArrayRef[Str])-
Optional.
Installs an alias for the candidate, bypassing multimethod dispatch. (But not bypassing the checks, coercions, and defaults in the signature!)
method
(Bool)-
Optional, defaults to 1.
Indicates whether the multimethod should be treated as a method (i.e. with an implied
$self
). Defaults to true, butmethod => 0
can be given if you want multifuncs with no invocant.Multisubs where some candidates are methods and others are non-methods are not currently supported! (And probably never will be.)
score
(Int)-
Optional.
Overrides the constrainedness score calculated as described in the dispatch technique. Most scores calculated that way will typically between 0 and 100. Setting a score manually to something very high (e.g. 9999) will pretty much guarantee that it gets chosen over other candidates when multiple signatures match. Setting it to something low (e.g. -1) will mean it gets avoided.
no_dispatcher
(Bool)-
Optional. Defaults to true in roles, false otherwise.
If set to true, Sub::MultiMethods will register the candidate method but won't install a dispatcher. You should mostly not worry about this and accept the default.
monomethod $name => %spec
monomethod($name, %spec)
is basically just a shortcut for multimethod(undef, alias => $name, %spec)
though with error messages which don't mention it being an alias.
multifunction $name => %spec
Like multimethod
but defaults to method => 0
.
monofunction $name => %spec
Like monomethod
but defaults to method => 0
.
Dispatch Technique
When a multimethod is called, a list of packages to inspect for candidates is obtained by crawling @ISA
. (For multifuncs, @ISA
is ignored.)
All candidates for the invoking class and all parent classes are considered.
If any parent class includes a mono-method (i.e. not a multimethod) of the same name as this multimethod, then it is considered to have override any candidates further along the @ISA
chain. (With multiple inheritance, this could get confusing though!) Those further candidates will not be considered, however the mono-method will be considered to be a candidate, albeit one with a very low score. (See scoring later.)
Any candidates where it is clear they will not match based on parameter count will be discarded immediately.
After that, the signatures of each are tried. If they throw an error, that candidate will be discarded.
If there are still multiple possible candidates, they will be sorted based on how constrained they are.
To determine how constrained they are, every type constraint in their signature is assigned a score. Any is 0. Defined inherits from Any, so has score 1. Value inherits from Defined, so has score 2. Etc. Some types inherit from a parent but without further constraining the parent. (For example, Item inherits from Any but doesn't place any additional constraints on values.) In these cases, the child type has the same score as its parent. All these scores are added together to get a single score for the candidate. For candidates where the signature is a coderef, this is essentially a zero score for the signature unless a score was specified explicitly.
If multiple candidates are equally constrained, child class candidates beat parent class candidates; class candidates beat role candidates; and the candidate that was declared earlier wins.
Method-resolution order (DFS/C3) is respected, though in Perl 5.8 under very contrived conditions (calling a sub as a function when it was defined as a method, but not passing a valid invocant as the first parameter), MRO may not always work correctly.
Note that invocants are not part of the signature, so not taken into account when calculating scores, but because child class candidates beat parent class candidates, they should mostly behave as expected.
After this, there should be one preferred candidate or none. If there is none, an error occurs. If there is one, that candidate is dispatched to using goto
so there is no trace of Sub::MultiMethod in caller
. It gets passed the result from checking the signature earlier as @_
.
Roles
As far as I'm aware, Sub::MultiMethod is the only multimethod implementation that allows multimethods imported from roles to integrate into a class.
use v5.12;
use strict;
use warnings;
package My::RoleA {
use Moo::Role;
use Sub::MultiMethod qw(multimethod);
use Types::Standard -types;
multimethod foo => (
positional => [ HashRef ],
code => sub { return "A" },
alias => "foo_a",
);
}
package My::RoleB {
use Moo::Role;
use Sub::MultiMethod qw(multimethod);
use Types::Standard -types;
multimethod foo => (
positional => [ ArrayRef ],
code => sub { return "B" },
);
}
package My::Class {
use Moo;
use Sub::MultiMethod qw(multimethod);
use Types::Standard -types;
with qw( My::RoleA My::RoleB );
multimethod foo => (
positional => [ HashRef ],
code => sub { return "C" },
);
}
my $obj = My::Class->new;
say $obj->foo_a( {} ); # A (alias defined in RoleA)
say $obj->foo( [] ); # B (candidate from RoleB)
say $obj->foo( {} ); # C (Class overrides candidate from RoleA)
All other things being equal, candidates defined in classes should beat candidates imported from roles.
CodeRef multimethods
The $name
of a multimethod may be a scalarref, in which case multimethod
will install the multimethod as a coderef into the scalar referred to. Example:
my ($coderef, $otherref);
multimethod \$coderef => (
method => 0,
positional => [ ArrayRef ],
code => sub { say "It's an arrayref!" },
);
multimethod \$coderef => (
method => 0,
alias => \$otherref,
positional => [ HashRef ],
code => sub { say "It's a hashref!" },
);
$coderef->( [] );
$coderef->( {} );
$otherref->( {} );
The $coderef
and $otherref
variables will actually end up as blessed coderefs so that some tidy ups can take place in DESTROY
.
API
Sub::MultiMethod avoids cute syntax hacks because those can be added by third party modules. It provides an API for these modules.
Brief note on terminology: when you define multimethods in a class, each possible signature+coderef is a "candidate". The method which makes the decision about which candidate to call is the "dispatcher". Roles will typically have candidates but no dispatcher. Classes will need dispatchers setting up for each multimethod.
Sub::MultiMethod->install_candidate($target, $sub_name, %spec)
-
$target
is the class (package) name being installed into.$sub_name
is the name of the method.%spec
is the multimethod spec. If$target
is a role, you probably want to includeno_dispatcher => 1
as part of the spec. Sub::MultiMethod->install_dispatcher($target, $sub_name, $is_method)
-
$target
is the class (package) name being installed into.$sub_name
is the name of the method.$is_method
is an integer/boolean.This rarely needs to be manually called as
install_candidate
will do it automatically. Sub::MultiMethod->install_monomethod($target, $sub_name, %spec)
-
Installs a regular (non-multimethod) method into the target.
Sub::MultiMethod->copy_package_candidates(@sources => $target)
-
@sources
is the list of packages to copy candidates from.$target
is the class (package) name being installed into.Sub::MultiMethod will use Role::Hooks to automatically copy candidates from roles to consuming classes if your role implementation is supported. (Supported implementations include Role::Tiny, Role::Basic, Moo::Role, Moose::Role, and Mouse::Role, plus any role implementations that extend those. If your role implementation is something else, then when you consume a role into a class you may need to copy the candidates from the role to the class.)
Sub::MultiMethod->install_missing_dispatchers($target)
-
Should usually be called after
copy_package_candidates
, unless$target
is a role.Again, this is unnecessary if your role implementation is supported by Role::Hooks.
Sub::MultiMethod->get_multimethods($target)
-
Returns the names of all multimethods declared for a class or role, not including any parent classes.
Sub::MultiMethod->has_multimethod_candidates($target, $method_name)
-
Indicates whether the class or role has any candidates for a multimethod. Does not include parent classes.
Sub::MultiMethod->get_multimethod_candidates($target, $method_name)
-
Returns a list of candidate spec hashrefs for the method, not including candidates from parent classes.
Sub::MultiMethod->get_all_multimethod_candidates($target, $method_name, $is_method)
-
Returns a list of candidate spec hashrefs for the method, including candidates from parent classes (unless
$is_method
is false, because non-methods shouldn't be inherited). Sub::MultiMethod->known_dispatcher($coderef)
-
Returns a boolean indicating whether the coderef is known to be a multimethod dispatcher.
Sub::MultiMethod->pick_candidate(\@candidates, \@args)
-
Returns a list of two items: first the winning candidate from an array of specs, given the args and invocants, and second the modified args after coercion has been applied.
This is basically how the dispatcher for a method works:
my $pkg = __PACKAGE__; if ( $ismethod ) { $pkg = Scalar::Util::blessed( $_[0] ) || $_[0]; } my ( $winner, $new_args ) = 'Sub::MultiMethod'->pick_candidate( [ 'Sub::MultiMethod'->get_all_multimethod_candidates( $pkg, $sub, $ismethod, ) ], \@_, ); $winner->{code}->( @$new_args );
BUGS
Please report any bugs to http://rt.cpan.org/Dist/Display.html?Queue=Sub-MultiMethod.
SEE ALSO
Class::Multimethods - uses Perl classes and ref types to dispatch. No syntax hacks but the fairly nice syntax shown in the pod relies on use strict
being switched off! Need to quote a few more things otherwise.
Class::Multimethods::Pure - similar to Class::Multimethods but with a more complex type system and a more complex dispatch method.
Logic - a full declarative programming framework. Overkill if all you want is multimethods. Uses source filters.
Dios - object oriented programming framework including multimethods. Includes a full type system and Keyword::Declare-based syntax. Pretty sensible dispatch technique which is almost identical to Sub::MultiMethod. Much much slower though, at both compile time and runtime.
MooseX::MultiMethods - uses Moose type system and Devel::Declare-based syntax. Not entirely sure what the dispatching method is.
Kavorka - I wrote this, so I'm allowed to be critical. Type::Tiny-based type system. Very naive dispatching; just dispatches to the first declared candidate that can handle it rather than trying to find the "best". It is fast though.
Sub::Multi::Tiny - uses Perl attributes to declare candidates to be dispatched to. Pluggable dispatching, but by default uses argument count.
Sub::Multi - syntax wrapper around Class::Multimethods::Pure?
Sub::SmartMatch - kind of abandoned and smartmatch is generally seen as teh evilz these days.
AUTHOR
Toby Inkster <tobyink@cpan.org>.
COPYRIGHT AND LICENCE
This software is copyright (c) 2020-2022 by Toby Inkster.
This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.
DISCLAIMER OF WARRANTIES
THIS PACKAGE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE.