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 ﬁnite 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 ﬁnite 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.