Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How do I specify a Perl 6 signature that encompasses everything I accept and excludes everything else?

Tags:

signature

raku

My assumption in this question is that a signature that I specify completely describes everything that a method will accept. Apparently I'm wrong about that, but I'd like to get that anyway. If I don't specify it in the signature, I don't want other people to be able to provide it as an argument thinking it will do something.

I create a signature with a single positional parameter for a method, and I expect it to only accepta single positional argument. However, it also accepts named parameters without complaint:

class SomeClass {
    method something ( Int $n ) { 'Ran something' }
    }

put SomeClass.something: 137; # Ran something
put SomeClass.something: 137, :foo('bar'); # Ran something

But, if I define a method that takes a named parameter, that's called each time I define it second. Also, although I think I've said it requires a named paramter foo, it's not required and it still accepts named parameters that I did not specify:

class SomeClass {
    multi method something ( Int $n ) { 'Ran something' }
    multi method something ( Int $n, :$foo ) { "Ran $foo" }
    }

put SomeClass.something: 137;  # Ran
put SomeClass.something: 137, :foo('bar'); # Ran bar
put SomeClass.something: 137, :bar('foo'); # Ran

So, some questions:

  • How do I specify a signature that encompasses everything I want to accept and excludes everything else?

  • How do I force Perl 6 to choose the closest matching signature?

  • In what order does Perl 6 decide to check the methods?

like image 224
brian d foy Avatar asked Jun 12 '17 04:06

brian d foy


2 Answers

You're touching on one of the things that is hardest to grasp about multi-method dispatch. Most important thing to realize is that every method signature has an implicit *%_ (aka a slurpy hash) if there isn't one specified. This means that it will eat any non-specific named parameters.

class A {
    method a() { dd %_ }  # %_ exists even if not specifically specified
}
A.a(:foo)   # {:foo}

Second thing you need to realize, is that named parameters only act as tie-breakers. So if there are multiple candidates that have the same matching set of positional parameters, MMD will use the first one that works (taking into account that all unexpected named parameters are slurped by %_):

class A {
    multi method a(:$foo) { say "foo" }
    multi method a(:$bar) { say "bar" }
}
A.a(:bar)    # foo

This seemingly unexpected result is caused by the fact that:

  1. both candidates have the same number of positional parameters
  2. the first candidate matches because :$foo is optional
  3. and the :bar is eaten by the implicit *%_

To make it work more like you expect, you need to put the candidates that need to be tie-broken into the order you would like them to fire, and make any named parameters mandatory:

class A {
    multi method a(:$foo!) { say "foo" }
    multi method a(:$bar!) { say "bar" }
}
A.a(:bar)    # bar

If you have candidates taking multiple named parameters, it quickly becomes rather complex and you're possibly better off by just having a method that uses introspection of %_:

class A {
    method a() {
        if %_<foo> {
            say "foo"
        }
        elsif %_<bar> {
            say "bar"
        }
        else {
            die "None of the signatures matched"
        }
    }
}
A.a(:bar)    # bar

Hope this makes things clearer :-)

like image 113
Elizabeth Mattijsen Avatar answered Oct 05 '22 08:10

Elizabeth Mattijsen


Methods always come with an implicit *%_ parameter, cf

say method {}.signature #=> (Mu $: *%_)

This is by design, with the idea that subclasses may pick out the arguments they like and then redispatch via nextsame et al (cf the design docs - not sure where or even if it's documented elsewhere).

There are several ways to reject undeclared named arguments, eg via a where clause

method m($positional, :$explicit, *% where !*) { ... }

or via an empty subsignature

method m($positional, :$explicit, *% ()) { ... }

When passing a non-declared argument, the former will fail with

Constraint type check failed in binding to parameter '<anon>'

and the latter with

Unexpected named argument '...' passed in sub-signature

I'll leave the exact semantics of multimethod dispatch for others to answer, but my heuristic rule of thumb is to make sure required named parameters are declared as such (ie the ! is present) and to put the more general methods last.

like image 35
Christoph Avatar answered Oct 05 '22 08:10

Christoph