I've missed something, I'm sure, but here's a brief explanation and a question at the end.
The more I play with refactoring with roles, the more surprises await me. Eliminating inheritance for roles is ridiculously easy. As it turns out, the only hard parts of this switch have been those bits of code where our multiple inheritance has provided "extraneous" methods and the roles fail in curious ways. Thus, we get exposure of some design flaws for free (but it's a far too much of a digression to describe right now).
Compare:
package Our::TV::Episode; # assume these use multiple inheritance, too use base qw( Audit Tags );
Versus:
package Our::TV::Episode; use Moose; extends 'Parent'; with qw( DoesAuditable DoesEditable DoesTagging DoesRelatedLinks );
We've flattened the hierarchy, we see all behaviors our class has and we see that many of these items clearly didn't belong in an "isa" relationship in the first place. Is this a good strategy? At first, I was tempted to say "not really, but it's better than the inheritance mess". However, I'm not sure that I still see this as an interim strategy. This might be a better design methodology. Perhaps even the base class can be eliminated. Every object would be composed of roles, with explicit method conflict resolution at compile time and instead of spaghetti code or ravioli code, perhaps we could think of this as "dim sum code", with every object being created via a tasty pick and choose menu of exactly what that object needs and nothing more.
Now you might look at the above and say "but tagging, auditing and that other stuff aren't part of a TV episode either! They don't belong there any more than they belonged in the inheritance tree." In real life that sounds reasonable, but code is written to fit our business needs first and only incidentally to model the real world. For example, as far as the BBC is concerned, even the most rabid Doctor Who fan has never watched an episode of Doctor Who in their life. You see, episodes have versions (e.g., "edited for adult content"), but you've never seen a version, either. Versions are broadcast at a set time or available on demand via iPlayer or similar technology. So you've not seen an episode, you've seen a broadcast or an ondemand. In the real world, you don't really think about all of this and you shouldn't. You don't need to know that brands have series and series might have more series and those have episodes and they all need to be audited, and tagged, and grouped into franchises and have rights modelling and so on and so on. You just want to watch Doctor Who. However, what I need to do to model your episode for our business needs and they don't always fit your Friday night on the sofa with two pints of lager and a packet of crisps.
Moving along, were I to eliminate base classes, that would also eliminate this silliness:
sub foo { my $proto = shift; my $class = ref $proto || $proto; die "You must override 'foo' in $class"; }
That fires at runtime, not compile time. Just adding a requires qw'foo'; to your role and this becomes a compile-time failure.
My question: what's wrong with this approach? It's so easy, so obvious and makes understanding code much easier that I can't see why I shouldn't go all the way with this. I'm throwing caution to the wind, so it would be nice if you'd throw some caution back at me.
Flattened, composable behavior -- sounds like traits.
;-)
-- dagolden
Re:have we heard this before?
autarch on 2009-03-18T15:18:22
I'm pretty sure that Stevan read some (or all) of those papers before working on roles in Moose.
Re:have we heard this before?
dagolden on 2009-03-18T18:16:58
I was sort of making fun of Ovid, since he wrote Class::Trait.
-- dagolden
Re:have we heard this before?
Ovid on 2009-03-18T18:58:46
Actually, while I wrote the first full-featured trait implementation that I'm aware of (though after others tried their hands at it), but it's Stevan who wrote Class::Trait. I just took over maintenance because I kept sending in bugs reports
:) Re:have we heard this before?
Ovid on 2009-03-18T15:26:52
I've read through that a number of times and I think they're right. There are, however, a couple of key differences. First, they don't have state with their traits (see bottom of page two in Traits: Composable Units of Behaviour). I think that's a mistake and so does Perl 6 and Moose. Smalltalk traits only share behavior, not data.
Second, they're not advocating eliminating inheritance. They're advocating eliminating MI and mixins and using traits for allomorphic properties. I'm thinking that perhaps they didn't go far enough, but maybe they're seeing something I'm not. That's really the question I need an answer to
:) The only problem I have with Perl 6 roles and (and Moose roles) is this:
#!/usr/bin/env perl -l
use strict;
use warnings;
{
package Foo;
use Moose::Role;
sub doit { print "foo" }
}
{
package Bar;
use Moose;
with 'Foo';
sub doit { print 'Bar' }
}
Bar->doit;That prints 'Bar'. It should be a compile-time failure. If I forget that a role implements a method with the same name as what I'm trying to implement, I've broken my code, badly. Worse, I've broken it silently. This isn't much better than MI or mixins, the techniques which roles (traits) were designed to fix. Roles are still better in general, but this is one design decision that I just can't understand.
Re:have we heard this before?
Stevan on 2009-03-18T16:47:09
I agree the local class overriding role can seem confusing, but over the past few years of using roles pretty heavily I have come to appreciate this feature quite a lot. It actually makes the roles more re-usable since it is very easy to locally override something. Yes it does destroy some of the black-box-ness of Roles, but honestly I have not found roles to be very useful unless you can look inside and see what they provide/do. I have come to kind of see Roles as being more semi-transparent then opaque, similar to how a superclass/subclass relationship must be.
- Stevan
Re:have we heard this before?
Ovid on 2009-03-18T16:54:30
But for the few times that a local class needs to override a role, why not just have the class explicitly exclude that method from the role? Same effect, but it's explicit, not hidden. So you avoid mysterious breakage and you get closer to how roles were originally intended to be used (a good thing, in this case).
I realize you probably can't change the API now, but what about use Moose::Role 'strict' or something like that?
Re:have we heard this before?
Stevan on 2009-03-18T19:23:21
What do you mean by "how roles were originally intended"? Because the traits papers describe local class overriding roles pretty specifically.
- Stevan
Re:have we heard this before?
Ovid on 2009-03-18T23:14:09
OK, I clearly need to go back and reread. Thanks!
Re:have we heard this before?
chromatic on 2009-03-18T19:36:56
[Why] not just have the class explicitly exclude that method from the role?
That's for the same reason that classes don't explicitly mark overridden methods as "Hey! I'm overriding this method here! Pay attention! Don't look in my superclass! I'm over heeeeere! CALLMECALLMECALLMECALLME!"
I can imagine a debugging or introspection mode where you can see exactly where methods and state come from, but one of the goals of roles was to provide transparent and typeful class componentization. If you don't know which methods a role provides, you shouldn't use it.
Re:have we heard this before?
Ovid on 2009-03-18T23:19:50
That's for the same reason that classes don't explicitly mark overridden methods as "Hey! I'm overriding this method here! Pay attention! Don't look in my superclass! I'm over heeeeere! CALLMECALLMECALLMECALLME!"
We know they don't do this in Perl, but we also know that Perl's core OO is fairly limited. In several other languages, you must explicitly mark overridden methods. It's a great help to the programmer to see what's going on. After all, programs should be written primarily for humans to read and only incidentally for computers to understand.
If you don't know which methods a role provides, you shouldn't use it.
Agreed. There's still a difference between the real world and the desired world and in the real world, things aren't that simple. I want my code to fail at compile time when it can do so in a way which costs less than the alternative. So far, aside from mocking my point of view, I've not heard anything which says that silently ignoring the role's method has a lower cost than the programmer explicitly ignoring it.
Re:have we heard this before?
chromatic on 2009-03-19T07:25:00
So far, aside from mocking my point of view, I've not heard anything which says that silently ignoring the role's method has a lower cost than the programmer explicitly ignoring it.
Let me put it forth then: requiring explicit confirmation of overriding would be inconsistent with the existing stages of implicit overriding. I'll even raise the question of consistency in a context unrelated to OO: do you worry that the binding of lexical variable declared in an innermost scope shadows a lexical variable of the same name declared in an outer scope, and does so without an explicit marking?
Re:have we heard this before?
Ovid on 2009-03-19T09:21:06
do you worry that the binding of lexical variable declared in an innermost scope shadows a lexical variable of the same name declared in an outer scope, and does so without an explicit marking?
Comparing a lexical variable and and a global method definition is comparing apples to oranges. When I'm calling $object->foo, I neither care, nor worry, what the lexicals are. I also don't worry about how &foo is defined, but I should care. And I do. With my heavy use of roles, I've found (an anecdote, I confess) that silently overriding a role method is a painful bug to track down. Knowing that, unlike with inheritance, we can actually find and prevent such errors at compile time but the keepers of the keys refuse to do so doesn't make my code magically work.
Re:have we heard this before?
huxtonr on 2009-03-19T17:09:59
With my heavy use of roles, I've found (an anecdote, I confess) that silently overriding a role method is a painful bug to track down. Knowing that, unlike with inheritance, we can actually find and prevent such errors at compile time but the keepers of the keys refuse to do so doesn't make my code magically work.
One of the advantages of a slick IDE would be that you could trap many of these mistakes at edit time even, rather than compile time. Purple method-names are overriding something, blue aren't (or whatever).
One of the things you get for free with Smalltalk's image system I suppose.
Re:have we heard this before?
chromatic on 2009-03-19T17:39:12
Comparing a lexical variable and and a global method definition is comparing apples to oranges.
I don't know what a "global method definition" is. Method definitions have their own scoping; they're scoped to invocants of the appropriate type. That's why I believe that the shadowing rule is appropriate: we're talking about which piece of code or behavior Perl can access when you use a specific name in a specific context.
Knowing that, unlike with inheritance, we can actually find and prevent such errors at compile time but the keepers of the keys refuse to do so doesn't make my code magically work.
That's because it's not an error. We designed them to work this way!
I suppose you could get what you want if you turned all of your roles into parameterized roles, where the parameters were a list of methods to compose (provided you had some control over the resolver and could check for any overriding), but I suspect you'd find that incredibly tedious, I'm sure you'll run into order-of-compilation-implies-symbol-visibility problems at least in Perl 5, and likely few other people would want that behavior anyway.
Re:have we heard this before?
dagolden on 2009-03-18T18:27:54
I thought that part of the point of having traits flatten at compile time was to require that kind of override to have explicit disambiguation. So you can locally override, but it's called out in the code.
Maybe a bad example, but for the sake of debate:
{
package Bar;
use Moose;
with 'Foo' except 'doit';
sub doit { print 'Bar' }
}Excluding 'doit' from Foo without providing it in Bar would also be an error, of course.
-- dagolden
Re:have we heard this before?
Stevan on 2009-03-18T19:26:49
When traits/roles are applied to classes the local class overrides the role, but the role overrides the superclass. When traits/roles are applied to other traits/roles they are merged together into a composite role and that is where this kind of disambiguation is needed. This cookbook recipe shows both aliasing and exclusion of role methods, this is a feature directly taken from the traits papers but which Perl 6 has not decided to include.
- Stevan
Re:have we heard this before?
Ovid on 2009-03-18T23:22:25
This cookbook recipe shows both aliasing and exclusion of role methods, this is a feature directly taken from the traits papers but which Perl 6 has not decided to include.
You know, I was wondering why I hadn't seen that in Perl 6. What does that last bit mean, though? That Perl 6 is not yet decided or that they've decided not to offer this? (I'll try and reread the spec tomorrow)
Re:have we heard this before?
Stevan on 2009-03-20T00:36:26
Perl 6 does not provide ways to explictly exclude or alias methods during compsition (well not that I have seen anyway, cause I don't follow it much these days).
- Stevan
I know I keep saying this - but this _really_ reminds me of the way Eiffel puts things together (although they keep the class metaphor rather than calling them roles).
Re:Go look at Eiffel
Ovid on 2009-03-18T15:43:17
Except that you still have MI (but with explicit method resolution) and you still can have behavior buried in inherited classes rather than explicitly stated in the preamble to your code (though I love that you must explicitly override them). Also, because it's class based, it's not as easy to share behavior amongst classes which are not structurally related -- one of the design goals of roles.
That being said, Eiffel looks really nice and if more than 3 people were using it
... ;) Re:Go look at Eiffel
Stevan on 2009-03-18T17:06:08
This book is actually one of my all time favorites. Eiffel has a nice concept of partial classes which themselves can require methods to be implemented, but it does not have conflict resolution like roles do so you are still susceptible to some of the issues that traditional MI/mixins has. But actually, if you throw in the rename and redefine keywords in Eiffel and you can actually manually fix that if you want. TIMTOWTDI I guess
:) - Stevan
Re:roles?
Ovid on 2009-03-18T19:02:58
For a variety of reasons they're significantly different. Amongst other issues, C++ and Java interfaces provide no implementation, nor do they offer conflict resolution. See the link dagolden provided for an in depth tour of roles (known also as "traits").
Re:roles?
chromatic on 2009-03-18T19:32:34
That question makes it sound like interfaces in C++ and Java actually work. (I suppose they work as designed, which makes them that much worse.)
Moving along, were I to eliminate base classes, that would also eliminate this silliness:
sub foo {
my $proto = shift;
my $class = ref $proto || $proto;
die "You must override 'foo' in $class";
}That fires at runtime, not compile time. Just adding a requires qw'foo'; to your role and this becomes a compile-time failure.
Alternately, if the "foo" you're overriding there is (as it so often is) something like this:
sub foo_plugins {
return ("My::Foo", "External::Library::Foo");
}
MooseX::Role::Parameterized might be a good option. e.g.
use MyRole => { foo_plugins => ["My::Foo", "External::Library::Foo" ] };
# elsewhere...
package MyRole;
use MooseX::Role::Parameterized;
parameter foo_plugins => (
isa => 'ArrayRef[ClassName]',
required => 1,
);
role {
my $p = shift;
method munge_stuff => sub {
# do something for each @{$p->foo_plugins}
};
};