The key to understanding roles is to note that roles don't implement
methods; classes implement methods.  Roles define which methods must
be implemented, and suggest ways that they might be implemented;
classes decide which implementation to use.  Anything that breaks this
paradigm is a Bad Thing.

Where things get slightly fuzzy is that there is one case where a
class is implicitly assumed to accept an implementation suggested by a
role - namely, if that implementation is the only one available.
Otherwise, the role is required to explicitly define a given method's
implementation.

David Green wrote:
> Jonathan Worthington wrote:
>> The spec is right in that you need to write a method in the class that
>> decides what to do. This will look something like:
>>
>>  method fuse() { self.Bomb::fuse() }
>
> That makes sense for using the combined role on its own, but can we still
> handle separate roles that get mixed together?  That is, after defining that
> method so I can call $joke.fuse(), can I still call $joke.Bomb::fuse and
> $joke.Spouse::fuse as well?

This is one of the distinctions between role composition and class
inheritance.  With class inheritance, the full tree of inherited
classes is publicly accessible; with role composition, the methods of
the combined role are the only ones that are made available to the
class.  In effect, a combined role acts as a middle manager that looks
over the available implementations and picks one, provides its own
alternative, or defers the decision to its boss (i.e., the class or
role into which it is composed).  Either way, once the class has
chosen an implementation, that is the implementation that will be
used.

As I understand it, the reason for this has more to do with attributes
than with methods: with role composition, you want to be able to "cut
away" any attributes that have become extraneous to the
implementations defined in the class.  E.g.:

    role R {
        has $foo;
    }

    class C does R {
        method foo() is rw { doIO() }
    }

The idea here is that C has chosen to implement foo by querying an
outside source (such as a database) whenever a read request is made of
it, and by sending information to an outside source whenever a write
request is made.  It never refers to the internal state that R
defined.  As such, there's no reason for C to set aside memory to
track an internal state.  You can't do this if someone is allowed to
explicitly call R::foo from any object of class C, overriding C's
choice as to how foo should be implemented.

> I'm thinking that it's useful to be able to refer to the fully-qualified
> names for anything composed into a role or class; often there will be no
> ambiguity, so the full name won't be necessary.  If the names aren't unique,
> then you can specify them fully, and perhaps add an unqualified "fuse()"
> that does one or the other (or both? or neither??) for convenience.  That
> shouldn't be necessary, I think -- it just means you would have to spell out
> exactly which method you wanted.

This is "class inheritance" think.  In "role composition" think, you
should never have to worry about how the composed roles might have
done things once composition is complete; you only concern yourself
with how the class does things.

> In the case Ovid also mentioned where two roles have a method of the same
> name because both methods really are doing the same thing, there ought to be
> a way to indicate that (though if they both come with implementations, you'd
> still have to pick one or write your own).
>
> Of course, in a perfect world, the common method would come from its own
> role: if Foo.fuse and Bar.fuse really mean Foo.Bomb::fuse and
> Bar.Bomb::fuse, then doing Foo and Bar together should automatically give
> you a single .fuse method (again, at least as far as the interface is
> concerned).

You have a point: it would be nice if you didn't have to engage in
unnecessary conflict resolution.  Of course, this may actually be the
case already; it all depends on how the compiler decides when to
complain about conflicting methods.

    role R { method foo() { say "foo" }
    role R1 does R { method bar() { say "bar" }
    role R2 does R { method baz() { say "baz" }
    class C does R1 does R1 { }

The question is whether or not Rakudo is smart enough to realize that
R1::foo is the same as R2::foo, or if it complains that R1 and R2 are
both trying to supply implementations for foo.  The former is the
desired behavior.

> I guess being able to create a role by dropping some bits from an existing
> role would be useful sometimes, but it seems to lose one of the most useful
> benefits of roles: as Jon Lang pointed out, "R1 :without<foo bar>" would
> effectively be a new role, one that doesn't do R1.  But you want something
> to do a role in the first place so that it will work anywhere else there is
> code looking for that role.

Obviously, someone who explicitly drops methods from a role isn't
concerned with the new role being usable wherever the original role
could be used (although there might be times when he'll be concerned
with the converse).  This is actually a separate topic that shouldn't
be conflated with the issue at hand; and I apologize for conflating
it.

If all you're trying to do is to avoid implementation conflicts, I'd
recommend "C does :blocking<foo bar> R1" instead of "R1 :without<foo
bar>": that approach would still insist that C implement foo and bar,
letting you use a C anywhere an R1 is called for; but it would
suppress R1's usual proposed implementations for foo and bar, avoiding
any chance of its proposals conflicting with any others.

Or perhaps it should be something along the lines of "Class does
:accepting<foo bar> Role", meaning that Class must implement all of
Role's methods, but will only consider Role's proposed implementations
for foo and bar; and (if possible) "Class does :!accepting<foo bar>
Role", meaning the same thing, except that the implementations of foo
and bar are the only ones _not_ under consideration.  (If :!accepting
wouldn't work that way, go with something like :rejecting instead.)

> So supposing:
>
>   role Canine { method bark { say "ruff"; } };
>   role Tree   { method bark { say "rough" } };
>
>   class Dogwood does Canine does Tree { ... };
>   my Dogwood $dw;
>
>   sub nighttime (Canine $rover) { $rover.bark if any(burglars()); }
>   sub paper (Canine $rex) { $rex.bark if newspaper(:delivered); }
>   sub paper (Tree $nodes) { $nodes.bark ==> flatten; ... }
>
> What happens when I call nighttime($dw)?  Obviously, it's meant to call
> $dw.Canine::bark. Since nighttime() is looking for something that does the
> Canine role, any method calls in that function can safely be assumed to be
> short for .Canine::bark (since all we know for sure is that any arg passed
> in will do Canine, and we can't know it will do anything else).
>
> If I want to call paper(), then I would have to cast $dw to either one of
> the roles, e.g. paper(Tree($dw)), and that would presumably strip off or
> hide the Canine part of its nature.  It doesn't seem unreasonable for any
> non-Tree attributes to be inaccessible inside paper(Tree), since all it can
> guarantee is the arg does Tree; but on the other hand, I think it would be
> OK but would require you to use the fully qualified names for anything
> non-Tree.

Again, this is class inheritance logic.  If you want to do this sort
of thing, you'd say:

    class Canine { method bark { say "ruff"; } };
    class Tree   { method bark { say "rough" } };
    class Dogwood is Canine is Tree { ... };
    my Dogwood $dw;

    sub nighttime (Canine $rover) { $rover.bark if any(burglars()); }
    sub paper (Canine $rex) { $rex.bark if newspaper(:delivered); }
    sub paper (Tree $nodes) { $nodes.bark ==> flatten; ... }

In this scenario, I could see nighttime calling Canine::bark (since
Dogwood::bark, the first choice, doesn't exist), and paper complaining
of ambiguity unless $dw is explicitly cast to either Canine or Tree -
although as written, I believe that the ambiguity is resolved by the
fact that Canine came first in the definition of Dogwood.

> If Dogwood defines its own .bark method, I can see it meaning one of two
> things: either it's yet another bark(), specifically $dw.Dogwood::bark(),
> that can be used only where we're expecting a Dogwood object.  (It might
> bear no relation to Canine::bark or Tree::bark, although in that case it
> would probably be a good idea to pick a different name!)  Or else, it could
> mean a consolidation of the two mixed-in .bark's, i.e. $dw.Canine::bark and
> $dw.Tree::bark would both now be implemented by plain $dw.bark, aka
> $dw.Dogwood::bark (all three full names would mean the same thing for
> Dogwood objects).

For role composition, the first thing to note is that Canine::bark and
Tree::bark are two very different things; so Dogwood::bark ought to
consider its context (am I being called to behave like a Canine, a
Tree, or something else?) and decide what to do based on that.  If
Dogwood::bark isn't defined, you should get an implementation conflict
error, because the class failed in its duty to provide an
implementation.  And if you _can't_ devise a Dogwood::bark method that
will behave properly in both contexts, you should rethink the Dogwood
class.

-- 
Jonathan "Dataweaver" Lang

Reply via email to