Role::Basic - what does DOES do?
Over the years it has become abundantly clear to me that people who object to OO fall into two categories:
- A handful of people who really understand object-oriented programming.
- Wankers with blogs.
Though I don't object to OO, it's still unclear which of the above categories I fall into. I suspect it's not the first.
This brings me to a lovely quote from H. L Mencken, "for every complex problem, there is a solution that is simple, neat, and wrong" (annoyingly, there are tons of subtle variations on that quote. I need to find the original). A "solution" I had in Role::Basic was was simple, neat, and wrong. I'm kicking myself, but my problem was my failure to apply what I thought I had "learned" from the original traits papers.
While porting Moose role tests to Role::Basic, I encountered the following tests. I was pretty happy because these were easy to convert over and I knew I was going to pass them.
package My::Role;
use Moose::Role;
sub foo { "FOO" }
sub bar { "BAR" }
package My::Role::Again;
use Moose::Role;
with 'My::Role' => {
-alias => { foo => 'baz', bar => 'gorch' },
-excludes => ['foo', 'bar'],
};
package My::Class::Again;
use Moose;
with 'My::Role::Again';
my $x = My::Class::Again->new;
isa_ok($x, 'My::Class::Again');
does_ok($x, 'My::Role::Again');
does_ok($x, 'My::Role');
can_ok($x, $_) for qw[baz gorch];
ok(!$x->can($_), '... cant call method ' . $_) for qw[foo bar];
is($x->baz, 'FOO', '... got the right value');
is($x->gorch, 'BAR', '... got the right value');
Nope. Turns out I had the same bug which Moose used to have back in 2008:
ok(!$x->can($_), '... cant call method ' . $_) for qw[foo bar];
I don't know why Moose had that but, I know why Role::Basic has this bug and I aim to fix it over the weekend. Specifically, it's because I wasn't paying attention to the underlying research and in thinking about it, I suspect that the DOES may not be implemented correctly.
To apply roles, I used this Menckenish code (actually, I didn't, but it's conceptually the same):
my @role_names = $class->fetch_all_roles($target);
foreach my $role_name (@role_names) {
$class->apply_role( $target, $role_name );
}
(Again, highly simplified example of the concept)
As I applied each role, I would take a look at the -excludes and -alias parameters and ensure that I respected them properly. However, I found that as roles composed other roles, I wasn't properly propagating the exclusions and aliases. That's obviously a problem, but how does one fix it?
By remembering the original traits research. As I noted a couple of days ago in What is a conflict?, a trait's identity is bound to the services it provides, not the trait's name. "Bob" and "Alice" might be the same person with a cross-dressing fetish. More importantly for us, two guys named "John" aren't necessarily the same guy.
In my code above, while I tried to respect aliasing and exclusion, I was mostly relying on trait names, not the traits themselves (Moose solves this by properly creating composite traits). I now have a partial local fix of this problem by respecting the fact that a trait is the services it provides, not its name.
That brings me to the problem of "DOES" (pseudo-code):
ClassA does RoleA -foo { // RoleA but excluding foo()
method foo() {...}
}
ClassB does RoleA {
}
Both ClassA and ClassB should return "true" for $class.DOES('RoleA'), but now we're relying on the name, not the actual trait. By definition, the above classes compose different traits. It turned out to be a mistake for me in Role::Basic internals to rely on the name; is it also a mistake in DOES?
I've been bitten many times by blithely ignoring issues and assuming they won't matter in the "real world". It reminds me of a time, many years ago, when I complained about MI being used to implement plugins in Catalyst and a dev told me something like "we're re-imagining OO". Well, it turns out that this bad idea was a bad idea and now they're re-imagining Catalyst:
Yet another time I was working on a project when a developer thought to solve a problem via something he called "chained inheritance". This was effectively diddling the inheritance tree at runtime. There is some dispute as to whether inheritance should be a core concept in OO, but there is little dispute that encapsulation (isolation!) should be a core concept and inheritance is a whopping violator of encapsulation. His simple and neat solution offered plenty of bugs due to the encapsulation violation and I vetoed it.
I'm not immune to this. I knew that AUTOLOAD was bad, but the first (non-public) version of my HTML::TokeParser::Simple module used it and I thought I was being clever by ignoring something I was told was bad. You will no longer find AUTOLOAD in that module.
This all boils down to me channeling my inner Dominus: "DON'T IGNORE STUFF YOU DON'T UNDERSTAND, RETARDO!".
So I thought I was being smart with my role application, but I was ignoring the foundation of roles in doing so. As a result, a bug appeared. I'm working hard to ensure that I better understand the original theory behind roles and I'm going to swallow my pride and not just "assume" I know what I'm doing.
But I still don't quite know what DOES should do.
Actually, multiple inheritance as a plugin system isn't necessarily a bad idea - in many cases, it works extremely well.
Both Catalyst and DBIx::Class did fine that way for some years - in the case of Catalyst, having already moved to be Moose based, there's simply no reason not to use roles rather than multiple inheritance - and a role based system seems to be easier for developers to reason about.
Of course, Role::Basic's refusal to permit method modifiers renders it entirely useless for such a plugin system since the entire point of them is to wrap application class methods to modify behaviour.
Therefore, if considering multiple inheritance versus roles for a plugin system, I would have to strongly recommend that you only consider a fully featured role system, such as those contained in Moose, Mouse or the Role::Tiny class in my Moo distribution.
-- mst, out.