Role::Basic - what is a conflict?
It's frustrating to me that I have limited bandwidth right now. A new job, a new country, a (soon to be) new baby, and a new blogging venture have all conspired to ensure that I don't have as much time for writing software outside of work. However, I've been working hard on Role::Basic and following a suggestion from Stevan Little, I've started a branch with Moose::Role tests and have started working through them. Some are clearly inapplicable (for example, Role::Basic has no notion of attributes), but a few have given me pause. I wasn't sure if they were supporting original traits behavior or if they were extensions for Moose. Since I've already ready the original traits paper (pdf) several times, I decided to move on to Traits: The Formal Model (pdf) to make sure I hadn't missed anything. There are some interesting bits there.
What caught me off guard in Moose was the following behavior. If a role providing method M consumes one other role providing method M, we get a conflict:
package Some::Role;
use Moose::Role;
sub bar { __PACKAGE__ }
package Some::Other::Role;
use Moose::Role;
with 'Some::Role';
sub bar { __PACKAGE__ }
package Some::Class;
use Moose;
with 'Some::Other::Role';
package main;
my $o = Some::Class->new;
print $o->bar;
Compare that to this code which produces no conflict:
package Some::Role;
use Moose::Role;
sub bar { __PACKAGE__ }
package Some::Other::Role;
use Moose::Role;
sub bar { __PACKAGE__ }
package Another::Role;
use Moose::Role;
with qw(Some::Role Some::Other::Role);
sub bar { __PACKAGE__ }
package Some::Class;
use Moose;
with 'Another::Role';
package main;
my $o = Some::Class->new;
print $o->bar;
Without reading further, can you figure out why this might be? According to the formal traits model, the above code should not conflict, but for different reasons than those listed in the Moose documentation. In fact, the first example also should not conflict, but I strongly suspect this would defy most developer's expectations. Bear with me because it's going to take a bit of explaining (and most of the time, a similarly structured example would conflict. I simply have a strange edge case where the theoretical meets reality).
If that a Moose role providing method M consumes two or more roles also providing method M, we get no conflict and Jesse Leuhrs pointed out to me that this is documented in the "Composition Edge Cases" section of the Moose role spec. Essentially, the conflicting code worked as I expected. However, the non-conflicting code is an edge case whereby the two or more roles consumed by the first role have method M in conflict and that automatically is converted to a required method. The consuming role, having a method M present, satisfies that requirement.
What is a conflict?
This behavior confused me and that's why I decided to go back to the source research and make sure I understood what was happening. The following information is taken straight from the Traits: The Formal Model paper I mentioned earlier, with the exception that I'm applying this to Perl (the authors stated explicitly that their model is language-agnostic).
Let's actually take a look at the roles in the first bit of code again:
package Some::Role;
use Moose::Role;
sub bar { __PACKAGE__ }
package Some::Other::Role;
use Moose::Role;
with 'Some::Role';
sub bar { __PACKAGE__ }
Clearly we can see this is in conflict. In both Moose and Role::Basic, you'll get an error. However — and this is a very important point — this code should not be in conflict. Traits provide services and a service is the binding of a label to a method. A label, in this case, is the method name ("bar") and the { __PACKAGE__ } is the actual method. Section 3.1 of the paper explains the basics of a conflict:
When traits are composed with +, labels that are bound to different methods will conflict...
In section 3.2, they explain "different methods" a bit more:
... we make no assumption on how equality of methods is tested in an actual language: m1 = m2 might be equality of strings, of syntax trees, of bytecode, or of something else.
In both Moose::Role examples above, we theoretically should have no conflict because the same label is bound to identical methods. However, for reasons of practicality, we don't actually do this and instead we have a conflict when methods of the same name are found.
Why is this so important? Because this is a formal paper and it must take the time to be very explicit in defining its model. As a result, things we might take for granted are made very clear. So the authors sort of punted on the definition of method equality because it's language-specific, but they were far more clear on the notions of class and role equality. For example:
Two classes are equivalent if they provide the same set of services (labels bound to methods), and all the methods reachable by sequences of self- and super-sends are the same.
And for traits:
Since traits are just finite mappings [of services], two traits are equal when these mappings are equal, that is, when equal labels map to equal methods
So far this is all fine. Like equals like. It's also important to note that the name of the trait isn't important. In natural language, "five" and "cinq" are the same thing: the name we give it doesn't change the concept.
There are really no surprises here. However, how exactly do they compose? Is trait composition commutative? In other words, does TraitA + TraitB mean the same as TraitB + TraitA? Yup, that's covered in section 3.4.
Is trait composition associative? In other words, does (TraitA + TraitB) + TraitC mean the same as TraitA + (TraitB + TraitC)? Again, the same section says this is true.
This is where I'm in a quandary, but I think I have a way out. Specifically, in the second example above, we have something like this:
package Another::Role;
use Moose::Role;
with qw(Some::Role Some::Other::Role);
Those all provide a bar method. However, lets assume that those three methods with the same "bar" label all have different bodies. According to the associative property, I think that means that the above should be equivalent to this:
package Some::Role;
use Moose::Role;
with qw(Another::Role Some::Other::Role);
However, this isn't what happens in Moose, as was demonstrated above. I got stuck in my testing because currently in Role::Basic, that's a conflict which must be resolved with something like this:
package Some::Role;
use Moose::Role;
with 'Another::Role' => { -excludes => 'bar' },
'Some::Other::Role' => { -excludes => 'bar' };
So doesn't this still violate the property of associativity? No, because as I mentioned earlier, the name of a trait has no bearing on the equality of traits. What's important is this bit:
Since traits are just finite mappings [of services], two traits are equal when these mappings are equal, that is, when equal labels map to equal methods
Since in the above code we have two traits which have had one of their services removed, they're no longer equivalent to the original trait bearing the same name and the model is not violated.
Conclusion
What does all of this mean for Role::Basic? First, the above is simply my interpretation of the paper and it's possible I've missed something. Second, I want to ensure that I am as compatible with Moose as possible. In this case, I may have dodged a bullet if I follow the original Traits model and require this syntax:
package Some::Role;
use Role::Basic;
with 'Another::Role' => { -excludes => 'bar' },
'Some::Other::Role' => { -excludes => 'bar' };
This is compatible with Moose behavior and it's explicit in its intent. However, it's also more typing and I can see that some people won't like this. There's nothing wrong with the Moose approach, but as I have somewhat different design goals here, I'm considering a slightly different solution (one which, in my experience, will not be encountered often). Trying to puzzle through design issues like this are making my limited bandwidth even more limited.
Suggestions are welcome here. Should I follow the Moose behavior or the original traits paper? I think maintaining associative and commutative properties is important — it will certainly make refactoring code easier — but the more different I make things, the less attractive Role::Basic might be to those who want just roles.
Why not allow syntax like Moose does?
with [qw( Another::Role Some::Other::Role )] => { -excludes => 'bar' };
Alternatively, another keyword before that, like this:
supressinroles qw( bar ); with qw( Another::Role Some::Other::Role );
I guess the really right answer would be to require methods added to resolve conflicts to be marked as such. Methods without such markers be considered to contribute to the conflict rather than to resolve it. (Likewise marking methods as a resolution when no conflict existed would be an error.)
So one of the things I found difficult when transposing the role models found in the papers to Perl space was that while they did try to be language agnostic, they really weren't.
They were prejudiced by their Smalltalk roots and the fact that when they went to implement this they had all the deep access that Smalltalk gives them. I found much of the same difficultly when I tried to port CLOS to Perl (see also: Moose). In the end things had to be Perl-ish and had to make sense in a Perl context.
This is why there is the difference of behaviors in the example you show. Basically the act of composing a role into another role (
role Foo {}; role Bar does Foo {};
) is one thing, and the act of composing two roles together, in a role or a class, (role Foo {}; role Bar {}; role FooBar with Foo with Bar {};
) is another. In a more perfect Smalltalk world, where we have access not only to the entire object model, but the compiler itself, these two operations would be identical. But when you are constrained by the order in which Perl executes code and also bound to respect the Perl-ish expectations of your users, things are a little different. And really, to be honest, I think our approach makes more sense, which you yourself said:Believe me, I love, absolutely LOVE stealing great features from other languages, and sometimes I run into the limitations of Perl (not often, but it does happen). The trick is to take those limitations in hand and bend the feature until it makes sense in a Perl-ish context, otherwise you end up with something that really feels foreign and thats not a good thing.
So, to answer your ultimate question, I think you should follow Moose behavior. If for no other reason then that you can steal our test suite. But also it is the one which people know and are used too and, IMHO anyway, feels the most Perl-ish.
- Stevan
Hi Ovid,
You said:
"In both Moose::Role examples above, we theoretically should have no conflict because the same label is bound to identical methods"
But I believe in the code you are discussing:
By the time the "are the methods the same" code ran, the __PACKAGE__ symbol would have already been turned into a different string in each case, so the methods are actually not identical?
Of course you go on to say:
"However, for reasons of practicality, we don't actually do this and instead we have a conflict when methods of the same name are found."
So my point is, uh, pointless, unless of course Moose does do some kind of "are these methods identical?" check, in which case you may want to make bar() return a fixed string rather than __PACKAGE__ in your test code.
- Alex
(PS. styling this comment was incredibly painful, but it finally looks reasonable in the preview... I hope it looks the same when I click Submit.)
@Alex: sorry for the awkward formatting. The preview doesn't quite get it right.
And that was a nice catch about the __PACKAGE__ symbol. You are right about the substitution.