Skip to content
Fetching contributors…
Cannot retrieve contributors at this time
578 lines (404 sloc) 18.7 KB

Perl usually decides which function to call based on the name of the function or the contents of a function reference. This is simple to understand. Perl can also examine the contents of the arguments provided to decide which of several variants of a function--each with the same name--to call. In this case, the amount and types of the function's arguments help to distinguish between the variants of a function. This is called multidispatch, and the functions to which Perl can dispatch in this case are multis.

Javascript Object Notation (JSON) 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. Here you can find an implementation that turns Perl 6 data structures to JSON.

This snippet demonstrates how multis make the code simpler and more obvious.

The other way round, converting a JSON string to a Perl 6 data structure, is covered in the chapter sec:grammars.

This code defines a single multi sub named to-json, which takes one argument and turns that into a string. to-json has many candidates; these subs all have the name to-json but differ in their signatures. Every candidate resembles:

Which one is actually called depends on the type of the data passed to the subroutine. A call such as to-json(Bool::True) invokes the first candidate. Passing a numeric value of type Real instead invokes the second.

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 more work: it wraps its parameter in quotes 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 types of their 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

Candidates can specify more complex signatures:

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. 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 (here, Any). A nominal type is an actual class or role, as opposed to additional constraints in the form of code blocks.

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.

In the first call of a(3), the nominal types alone already determine the best candidate match, so the where block never executes and the first $counter output is always 0.

The output after the second call is at least 1. The compiler has to execute the where-block at least once to check if the third candidate is the best match, 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 execute is specific to any particular implementation of Perl 6.

Verify Rakudo * behavior at press time.

Narrowness

One candidate remains from the JSON example:

With no explicit type or constraint on the parameter $d, its type defaults to Any--and thus it 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), 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 a similar signature without 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 to find the candidate with the best matching signature by examining nominal type constraints. These are far cheaper to check than constraint checks. Constraint checking occurs next, and only if the constraint check of the narrowest candidate fails, other candidates are tried that are lass narrow by nominal type.

I could use a table or figure here to illustrate the hierarchy of narrowing.

The Int type object both conforms to Int, but it is also an undefined value. If you pass it to the multi a, the second candidate, which is specific to Int wins, because nominal types are checked first.

Multiple arguments

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 class for each possible symbol. For each combination of chosen symbols for which Player One wins there's a candidate of the form:

Because the bodies of the subs here do not use the parameters, there's no reason to force the programmer to name them; they're anonymous parameters. A single $ in a signature identifies 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 acts as 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 a subtype of the first.

In 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).

Bindability checks

Traits can apply implicit constraints:

This routine exchanges the contents of its two arguments. It must bind the two arguments as rw--both readable and writable. Calling the swap routine with an immutable value (for example a number literal) will fail.

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:

Nested Signatures in Multi-dispatch

Chapter sec:subs showed how to use nested signatures to look deeper into data structures and extract parts of them. In the context of multiple dispatch, nested signatures take on a second task: they act as constraints to distinguish between the candidates. This means that it is possible to dispatch based upon the shape of a data structure. This brings Perl 6 a lot of the expressive power provided by pattern matching in various functional languages.

Some algorithms have very tidy and natural expressions with this feature, especially those which recurse to a simple base case. Consider quicksort. The base case is that of the empty list, which trivially sorts to the empty list. A Perl 6 version might be:

The [] declares an empty nested signature for the first positional parameter. Additionally, it requires that the first positional parameter be an indexable item--anything that would match the @ sigil. The signature will only match if the multi has a single parameter which is an empty list.

The other case is a list which contains at least one value--the pivot--and possibly other values to partition according to the pivot. The rest of quicksort is a couple of recursive calls to sort both partitions:

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..

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. This delegation is common in object destructors, where each subclass may perform some cleanup for its own particular data. After it finishes its work, it can delegate to its parent class meethod by calling nextsame.

Multiple MAIN subs

MAIN routines can also be multis. That way it is easy to add separate code for each action that the script can perform.

Example calls to this script:

Here the multi dispatcher selects the routine based on the value of the first argument.

POD ERRORS

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

Around line 1:

Unknown directive: =head0

Around line 24:

Deleting unknown formatting code A<>

Around line 123:

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

Around line 187:

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

Around line 231:

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

Around line 276:

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

Around line 435:

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

Around line 452:

Deleting unknown formatting code A<>

Around line 509:

Deleting unknown formatting code N<>

Jump to Line
Something went wrong with that request. Please try again.