8

Maybe I'm missing something, but I'd like to know if there is a good reason why this code should compile

role L {
  method do-l (Int, Int --> Int ) { ... }
}

class A does L {
  method do-l (Int $a, Real $b --> Str) {
    .Str ~ ": Did you expect Int?" with $a + $b
  }
}

my $a = A.new;

say $a.do-l: 2, 3.323

This will output

5.323: Did you expect Int?

I was curious if someone know a way for the compiler to at least throw some warning with the implemented signature of the role L.

margolari
  • 651
  • 3
  • 11
  • 2
    What is your expected output? What you've shown is what I would expect. The `L.do-l` has a method that doesn't match, and in any case, the class method takes precedence, so on both bases, `A`'s `do-l` should be called. The addition of an `Int` and a `Real` will be a `Real`. – user0721090601 Apr 11 '20 at 18:17
  • 1
    Ah, now I see. You're actually asking about the yada operator `...` (admittedly, while it makes sense in production code, here it can cause confusion) – user0721090601 Apr 11 '20 at 20:33
  • You can't instantiate roles if one method is stubbed, but there's no problem in instantiating classes. ` class L { method yadda {…}}; say L.new.raku` – jjmerelo Apr 12 '20 at 08:30
  • 2
    "You can't instantiate roles if one method is stubbed" Readers may misinterpret this comment. You can't instantiate roles, period. They're just fragments of a class. If you try to instantiate a role, raku presumes you mean to instantiate an empty class that does that role, and does that for you. Naturally, this fails if the role contains a method that's stubbed, because enforcement is the whole point and meaning of a stubbed method in a role. In contrast, stubbing in a class just means "I haven't written this code yet". – raiph Apr 12 '20 at 10:08
  • @raiph, you can "pun" them, which is akin to instantiating them... The mechanism is transparent, I guess. – jjmerelo Apr 13 '20 at 06:34
  • 2
    @jjmerelo Yes, it's a [type pun](https://en.wikipedia.org/wiki/Type_punning) ("subverts or circumvents the type system"). The one we're talking about here (`role foo {}.new` being automatically converted into something like `anon class foo does role foo {}.new` ) is so transparent that folk may not realize it's a pun, hence my prior comment. It's nice that this pun single-handedly enables the primary alternative to inheritance and to delegation, namely organizing and constructing types using *composition* instead, but imo it's important folk don't entirely lose sight that the `.new` is a pun. – raiph Apr 13 '20 at 13:24

2 Answers2

6

throw some warning with the implemented signature of the role L.

You get that if you prefix the method declaration with multi:

role L {
  multi method do-l (Int, Int --> Int ) { ... }
}

With this your program displays:

===SORRY!=== Error while compiling ...
Multi method 'do-l' with signature :(A: Int $, Int $, *%_ --> Int)
must be implemented by A because it is required by a role ...

I'd like to know if there is a good reason why this code should compile [without the multi]

I think the design intent was to support two notions of polymorphic composition:

  • Without the multi, enforcement only relates to existence of a method with the right name; parameters are ignored.

  • With the multi, enforcement covers the name and all parameters as well (or some).

My personal take on whether there's a good reason for:

  • Supporting two flavors of method polymorphism? Sometimes enforcing strict adherence to the signature is helpful. Sometimes it gets in the way.

  • Distinguishing them via multi? Full signature enforcement requires that implementing classes/roles have a method with exactly the same signature. But what if an implementing class/role wants to handle an int instead of Int for a parameter? Raku compromises. Provided an implementing class/role has an exactly compliant method it can also have variations. The perfect way to convey this is to prefix a stubbed method with multi.

  • Having the default be name only polymorphism? We could have chosen multi semantics as the default, and had users write an only prefix if they wanted name only polymorphism. But that would reverse the usual situation (i.e. ignoring stubbed methods). More generally, the intent is that Raku provides a wide range of stricture for its features, from relaxed to uptight, and picks a default for any given feature that is judged right based on feedback from users over the years.

What if the default doesn't seem right? What if the existing range of strictures isn't enough? What if one group thinks we should go left and another thinks we should go right?

Raku has (imo) remarkable governance mechanisms for supporting user driven language evolution. At the top level there are elements like the braid architecture. At the bottom level there are elements like versionable types. In the middle are elements like RoleToClassApplier which mediates the process of applying a role to a class, which is the point at which a required method needs to be found or the class's construction will fail. In short, if the language doesn't work the way you want, including such things as strictures, you can, at least in principle, change it so it does.

raiph
  • 31,607
  • 3
  • 62
  • 111
  • 1
    Wow this is exactly the piece of information I was looking for, I wished I had read about this in the raku docs for roles, or maybe I missed, if this is not there it would surely help many people writing concise roles, since at first sight having to put ```multi``` in ths stub is unintuitive, but now thanks to your explanation it actually makes sense. – margolari Apr 12 '20 at 16:15
  • 1
    @margolari It's good to hear you got the info you needed. See also https://github.com/Raku/doc/issues/3330 – raiph Apr 12 '20 at 23:02
2

I'm assuming here you're asking about why there's no warning with respect to the stubbing. Indeed, normally a stubbed method must be implemented — but that's it.

You can see how the roles are composed into class here in Rakudo's source ($yada basically means $is-stubbed):

if $yada {
    unless has_method($!target, $name, 0)
            || $!target.HOW.has_public_attribute($!target, $name) {
        my @needed;
        for @!roles {
            for nqp::hllize($_.HOW.method_table($_)) -> $m {
                if $m.key eq $name {
                    nqp::push(@needed, $_.HOW.name($_));
                }
            }
        }
        nqp::push(@stubs, nqp::hash('name', $name, 'needed', @needed, 'target', $!target));
    }
}

So you can see that the logic is just to see if a method exists with the same name. It's definitely possible to write a module that would update this logic by augmenting the apply() method or outright replacing the RoleToClassApplier class. However, it might be difficult. For example, consider:

class Letter { }
class A is Letter { } 
class B is Letter { }

role Foo {
  method foo (Letter) { ... }
}

class Bar does Foo { 
  method foo (A) { 'a' }
  method foo (B) { 'b' }
}

Should we consider the stubbed method to be correctly implemented? But someone else could later say class C is Letter and suddenly it's not implemented. (Of course, we could say that the best logic would be require, at the least, the identical or supertype, but that's more restictive than necessary for dynamic languages, IMO).

There isn't, AFAICT, a method that's called on roles during composition that also references the class (actually, there isn't any method called at all in add_method), so there's no way to write your own check within the role . (but I could be wrong, maybe raiph, lizmat or jnthn could correct me).

My recommendation in this case would be to, instead of stubbing it, simply add in a die statement:

role L {
  method do-l(Int $a, Int $b --> Int) {
    die "SORRY! Classes implementing role L must support the signature (Int, Int)";
  }
}

This won't capture it at compilation, but if at any point another method in L needs to call on do-l(Int, Int) —even if the composing class implements other signatures—, it will get called and the error caught fairly quickly.

user0721090601
  • 5,276
  • 24
  • 41