Skip to content

Latest commit

 

History

History
528 lines (371 loc) · 17.3 KB

multi-dispatch.pod

File metadata and controls

528 lines (371 loc) · 17.3 KB

Javascript Object Notation, or JSON for short, is a simple data exchange format often used for communicating with web services. It supports arrays, hashes, numbers, strings, boolean values, and null, the undefined value.

The following example presents a section of JSON::Tiny, a minimal library used to convert Perl 6 data structures to JSON. See grammars for the other part of that module, which parses JSON and turns it into Perl 6 data structures.

The full code, containing additional documentation and tests, is available from http://github.com/moritz/json/.

This code defines a single multi sub named to-json, which takes one argument and turns that into a string. However there are many candidates of that sub: subs with the same name, but different signatures.

The various candidates all look like:

Which one is actually called depends on the type of the data passed to the subroutine. If you call to-json(Bool::True), the first one is called. If you pass a numeric value of type Real instead, the second one is called.

The candidate for handling Real is very simple; because JSON's and Perl 6's number formats coincide, the JSON converter can rely on Perl's conversion of these numbers to strings. The Bool candidate returns a literal string 'true' or 'false'.

The Str candidate does a bit more work: it adds quotes at the start and the end, and escapes literal characters that the JSON spec does not allow in strings -- a tab character becomes \t, a newline \n, and so on.

The to-json(Array $d) candidate converts all elements of the array to JSON -- with recursive calls to to-json, joins them with commas and surrounds them with square brackets. The recursive calls demonstrate a powerful truth of multidispatch: these calls do not necessarily recurse to the Array candidate, but dispatch to the appropriate candidate based on the type of their single arguments.

The candidate that processes hashes turns them into the form { "key1" : "value1", "key2" : [ "second", "value" ] }. It does this again by recursing into to-json.

Constraints

Link to Any discussion.

This candidate adds two new twists. It contains no type definition, in which case the type of the parameter defaults to Any, the root of the "normal" branch of the type hierarchy. More interestingly, the where {!defined $d} clause is a constraint, which defines a so-called subset type. In this case, this candidate will match only some values of the type Any -- those where the value is undefined.

Whenever the compiler performs a type check on the parameter $d, it first checks the nominal type (which is Any here). If that check succeeds, it calls the code block. The entire type check can only succeed if the code block returns a true value.

The curly braces for the constraint can contain arbitrary code. You can abuse this to count how often a type check occurs:

This code defines three multis, one of which increases a counter whenever its where clause executes. Any Perl 6 compiler is free to optimize away type checks it knows will succeed. In the current Rakudo implementation, the second line with say will print a higher number than the first.

The first $counter output is always 0, since the nominal types alone already determine which candidate matches best, so the where-block is never executed.

The second output is at least 1. The compiler has to execute the where-block at least once to check if the third candidate can be called, but the specification does not require the minimal possible number of runs. This is illustrated in the second $counter output. The specific implementation used to run this test actually executes the where-block twice. Keep in mind that the number of times the subtype checks blocks are executed is implementation specific.

Verify Rakudo * behavior at press time.

Narrowness

There's one candidate not yet explained from the JSON example:

This has no explicit type or constraint on its parameter at all, so it defaults to Any -- and thus matches any passed object. The body of this function complains that it doesn't know what to do with the argument. This works for the example, because JSON's specification covers only a few basic structures.

The declaration and intent may seem simple at first, but look closer. This final candidate matches not only objects for which there is no candidate defined, but it can match for all objects, including Int, Bool, Num. A call like to-json(2) has two matching candidates -- Int and Any.

A and B are abstract; how about Integer and Positive Integer? That has its flaws too, but it's more concrete. It also fixes the subsequent example.

Good idea, but Positive Integer would be a subset type, which doesn't count as a narrower nominal type. Array and List would work, but are hard to get right. Int ~~ Num is subject to change, so currently I can't think of a good idea with builtin types - any other idea? --moritz

If you run that code, you'll discover that the Int candidate gets called. Because Int is a type that conforms to Any, it is a narrower match for an integer. Given two types A and B, where A conforms to B (A ~~ B, in Perl 6 code), then an object which conforms to A does so more narrowly than to B. In the case of multi dispatch, the narrowest match always wins.

A successfully evaluated constraint makes a match narrower than in the absence of a constraint. In the case of:

... an undefined value dispatches to the second candidate.

However, a matching constraint always contributes less to narrowness than a more specific match in the nominal type.

This restriction allows a clever compiler optimization: it can sort all candidates by narrowness once, and can quickly find the candidate with the best matching signature by examining nominal type constraints, which are very cheap to check. Only then will it run the constraint checks (which tend to be far more expensive). If they fail, it considers candidates that are less narrow by nominal types.

With some trickery it is possible to get an object which both conforms to a built-in type (Num, for example) but which is also an undefined value. In this case the candidate that is specific to Num wins, because the nominal type check is narrower than the where {!defined $d} constraint.

Multiple arguments

Multi dispatch is not limited to one parameter and argument. Candidate signatures may contain any number of positional and named arguments, both explicit and slurpy. However only positional parameters contribute to the narrowness of a match:

This example demonstrates how multiple dispatch can encapsulate all of the rules of a popular game. Both players independently select a symbol (rock, paper, or scissors). Scissors win against paper, paper wraps rock, and scissors can't cut rock, but go blunt trying. If both players select the same item, it's a draw.

The code creates a type for each possible symbol by declaring an enumerated type, or enum. For each combination of chosen symbols for which Player One wins there's a candidate of the form:

The only new thing here is that the parameters don't have names. The bodies of the subroutines do not use them, so there's no reason to force the programmer to name them. A single $ in a signature stands for an anonymous scalar variable.

The fourth candidate, multi wins(::T $, T $) { 0 } uses ::T, which is a type capture (similar to generics or templates in other programming languages). It binds the nominal type of the first argument to T, which can then act as a type constraint. If you pass a Rock as the first argument, T is an alias for Rock inside the rest of the signature and the body of the routine. The signature (::T $, T $) will bind only two objects of the same type, or where the second is of a subtype of the first.

In the case of this game, that fourth candidate matches only for two objects of the same type. The routine returns 0 to indicate a draw.

The final candidate is a fallback for the cases not covered yet -- every case in which Player Two wins.

If the (Scissors, Paper) candidate matches the supplied argument list, it is two steps narrower than the (Any, Any) fallback, because both Scissors and Paper are direct subtypes of Any, so both contribute one step.

If the (::T, T) candidate matches, the type capture in the first parameter does not contribute any narrowness -- it is not a constraint, after all. However T is a constraint for the second parameter which accounts for as many steps of narrowness as the number of inheritance steps between T and Any. Passing two Rocks means that ::T, T is one step narrower than Any, Any. A possible candidate:

... would win against (::T, T).

(TODO: If we're going to change the example to use an enum instead of classes, surely we need some explanation of how it can use an enum value instead of a type? (I would take a stab at writing this myself, except I have no idea how/why it works.))

Bindability checks

Traits can apply implicit constraints:

This routine exchanges the contents of its two arguments. To do that is has to bind the two arguments as rw -- both readable and writable. Trying to call the swap routine with an immutable value (for example a number literal) fails.

The built-in function substr can not only extract parts of strings, but also modify them:

You already know that the three-argument version and the four-argument version have different candidates: the latter binds its first argument as rw:

The discussion of slurpy versus optional parameters seems out of place here; functions chapter?

This is also an example of candidates with different arity (number of expected arguments). This is seldom really necessary, because it is often a better alternative to make parameters optional. Cases where an arbitrary number of arguments are allowed are handled with slurpy parameters instead:

Protos

You have two options to write multi subs: either you start every candidate with multi sub ... or multi ..., or you declare once and for all that the compiler shall view every sub of a given name as a multi candidate. Do the latter by installing a proto routine:

Nearly all Perl 6 built-in functions and operators export a proto definition, which prevents accidental overriding of built-insOne of the very rare exceptions is the smart match operator infix:<~~> which is not easily overloadable. Instead it redispatches to overloadable multi methods..

Multi Methods

Methods can participate in dispatch just as do subroutines. For multi method dispatch the invocant acts just like a positional parameter.

The main difference between sub and method calls is where the dispatcher searches for the routines: it looks for subroutines in the current and outer lexical scopes, whereas it looks for methods in the class of the invocant and recursively in any parent classes.

# XXX should this explanation moved to the OO tutorial?
# XXX     jnthn: in my opinion, yes

# TODO: Multi method dispatch example

Unlike subroutine dispatch, you can dispatch to multiple candidates with multimethods. The $object.?method syntax dispatches to zero or one matching candidates; it is no error if there is no matching candidate. $object.*method calls all matching candidates, but it is no error if there are no matching candidates. $object.+method calls at least one matching candidate.

Toying with the candidate list

Each multi dispatch builds a list of candidates, all of which satisfy the nominal type constraints. For a normal sub or method call, the dispatcher invokes the first candidate which passes any additional constraint checks.

A routine can choose to delegate its work to other candidates in that list. The callsame primitive calls the next candidate, passing along the arguments received. The callwith primitive calls the next candidate with different (and provided) arguments. After the called routine has done its work, the callee can continue its work.

If there's no further work to do, the routine can decide to hand control completely to the next candidate by calling nextsame or nextwith. The former reuses the argument list and the latter allows the use of a different argument list.

Which "this" is "This"? An example will clarify here.

This is often used if an object has to clean up after itself. A sub class can provide its own cleanup method for cleaning the own backyard, and then delegate to its parent class method by calling nextsame to do the rest of the un-dirtying work.

POD ERRORS

Hey! The above document had some coding errors, which are explained below:

Around line 1:

Unknown directive: =head0

Around line 3:

A non-empty Z<>

Around line 16:

Deleting unknown formatting code U<>

Around line 106:

=end for without matching =begin. (Stack: [empty])

Around line 168:

=end for without matching =begin. (Stack: [empty])

Around line 212:

=end for without matching =begin. (Stack: [empty])

Around line 369:

=end for without matching =begin. (Stack: [empty])

Around line 424:

=end for without matching =begin. (Stack: [empty])

Around line 457:

Deleting unknown formatting code N<>

Around line 490:

=end for without matching =begin. (Stack: [empty])

Around line 523:

=end for without matching =begin. (Stack: [empty])