Happy New Year/Roles!

Happy New Year everyone!

Here it is, only a few hours into the new year, and I'm already breaking a resolution of sorts. It wasn't a proper resolution, but given that I have plenty of things on my plate — not the least of which is a baby on the way — I had decided that I wasn't going to hack on Role::Basic for a bit until I saw the dust settling.

First, a simple question: if you're interested in Role::Basic, is it as a stepping stone to Moose, or because you just want roles and nothing else? The answer to this question could have a huge bearing on a fundamental problem that I face. This post is going to be rather long, so if all you do is answer that question, that's fine. I need to know. The reason this post is going to be long is because making things hard is easy. Making things easy is hard. Thus, in trying to solve a problem in an "easy" way, I have to think about a hard problem.

The problem with waiting for the Role::Basic dust to settle is that there's a touch more dust than I thought. Via private email, Facebook and Twitter, I already have several people thanking me and telling me they're going to start using this module. It could work its way into my $work, there's already a module on the CPAN which lists Role::Basic as a dependency and though it only has 11 watchers, the Role::Basic github repository is already my most watched module (outside of HOP which is merely a port of Dominus' code). On top of that, Matt Trout has opened a bug report against Role::Basic, agreeing with its goals, but concerned it will cause confusion.

To add to the fuss, it took only three days after the initial release for a complaint about the same problem the original traits researchers hit, I need to properly handle state. That's where the real problem kicks in.

Traits and No State

The original traits paper stated:

Traits do not specify any state variables, and the methods provided by traits never directly access state variables.

Reading further into the paper, it makes it clear that traits are allowed to access state via getters and setters and these are added to the list of methods the trait requires but the trait must never define any state of their own. The reasoning behind this was simple: if traits could define their own state and don't know about other traits, you could still have two or more traits attempting to define the same state and effectively get you back to the sort of conflicts that multiple inheritance could cause (this might seem strange to Perl 5 programmers, but it's as if two roles reached inside of a blessed hashref to diddle the same "_counter" slot).

Thus, in the original traits model, we the following:

class = state + traits + glue

Since traits were not allowed to manipulate state variables directly, any manipulation went into that "glue". If a trait wanted its own state, all it could do is list methods which it required and have the class implement them in that "glue". Thus, Marcel Grünauer had a problem because some of his traits, ahem, "roles", wanted state, but he couldn't do this:

package My::Role;
use Class::Accessor::Lite rw => [qw(foo bar)];
use Role::Basic;

This is because the role has no way of knowing if methods defined outside of the role are "helper functions" or getters/setters. The role only provides methods defined in that role or defined in another role the first role consumes. According to the original traits model, the "proper" solution to Marcel's problem is the following:

package My::Role;
use Role::Basic;
requires qw(foo bar);

And in the consuming class:

package My::Class;
use Role::Basic 'with';

# this is the infamous "glue" code we're talking about it
use Class::Accessor::Lite rw => [qw(foo bar)];
with 'My::Role';

That works, but there are a couple of issues with this. It turns out that not allowing traits to define state was a difficult constraint and multiple ways of implementing stateful traits have been proposed. This was largely because programmers were getting frustrated at having to duplicate the glue in every class which used a given role. Weren't roles supposed to reduce that duplication? And if the role requires another accessor, now we have to go into every class consuming that role and add it to the "glue". Ugh! This is a sticky problem.

This also means that we're introducing some scaffolding that will mean more code changes will be necessary if/when you wish to upgrade from Role::Basic to Moose.

A Prelude to Moose?

This all leads to my wondering if the interest expressed is just for roles or a prelude to Moose. If this module is a "prelude to Moose", then I should strive to make the upgrade path as easy as possible. Should I provide a "has" subroutine so you can declare attributes? But if I do that, you need to be able to set them in the constructor which implies I should provide a default constructor which implies that creeping featuritus has struck and I'm in danger of working on reimplementing Moo or Mouse. This is bad. I just wanted to make it easy to use roles.

If, however, people want "just roles" and not a prelude to Moose, then some artificial constraints could go away. For example, in Matt Trout's bug report, he mentioned:

Additionally, doesn't -excludes entirely exclude the method? Don't we actually want a form that says "I am going to provide my own version so convert it into a requires"?

I agree with Matt on this one. I had brought this issue up about a year and a half ago,¹ but I think it got lost in the wake of other discussions. However, I deliberately did not introduce this feature because Moose doesn't implement it. The following on my machine prints 1.21:

{
    package Some::Role;
    use Moose::Role;

    sub foobar { print "hi!" }
}
{
    package Some::Class;
    use Moose;
    with 'Some::Role' => { -excludes => 'foobar' };
}
my $o = Some::Class->new;
print Moose->VERSION;

If you later try to call the "foobar" method, you will get the standard "Can't locate object method" runtime error instead of a composition time error. This should also apply to the '-alias' feature because of this:

if ( $object->DOES('Some::Role') ) {
    # you've promised that the object does this role
    # the consumer has to assume that the object
    # provides the role's behavior
}

I actually think I can implement this and it won't break the upgrade path since we're being more restrictive rather than more permissive, but I want to think very carefully about this.

If I go the "roles only" route, I can have greater flexibility, but will need to evaluate everything on whether or not the expected benefit is greater than the cost of making it harder to convert to Moose. However, this seems like a bad idea if people are using this module as bait to convince others (or themselves) that shacking up with a Moose is a good idea.

In the meantime, this still doesn't give me a great solution to Marcel's problem. Rats!


1. I now see that I filed a bug about adding excluded methods to requirements, but it was considered to be a non-trivial problem and the ticket is still open.

11 Comments

I wonder whether my problem is actually about state. Had I written those accessors manually, there would be no problem even though they access the same hash slot as the generated versions.

An accessor generator like Class::Accessor::Lite is simply a helper that saves you from writing repetitive code, nothing more. The only problem I can see is that it's not actually exactly the same as writing those accessors manually because they are marked as coming from a different package.

Role::Basic is nice because it just does one thing; it's not concerned about generating accessors and, in my opinion, nor should it be.

So I can see two lightweight solutions: either we find a way to install a method in a different package without leaving a trace that it actually came from somewhere else - that is, the stash name obtained through svref_2object() should be the target package, not the accessor generator package.

The other way might be to tell Role::Basic that some stash names are ok, like

# provide all methods coming from this package
provide 'Class::Accessor::Lite';

or

# provide only those methods
provide qw(foo bar);

or

use Role::Basic provide => 'Class::Accessor::Lite';

or some such.

Role::Basic - Just roles. Nothing else.

:-)

I'd like Role::Basic for just roles with a minimum of dependencies to install. If I wanted to use Moose, I'd use Moose. :)

Make all Moose-like syntax exactly semantically equivalent to Moose.

Use Moose-incompatible syntax for semantics that are incompatible with Moose.

Do not borrow syntax from Moose while also changing its semantics.

That way, people can make a choice case by case about what capacity they want to use Role::Basic in. Those who want just roles can use the whole feature set and need not accept any limitations. Those who stick with Moose-like features in hopes of being able to switch to Moose will not have to carefully re-examine their code for subtle changes in behaviour if and when they do make the switch.

Scenario: Class Foo consumes RoleA and RoleB. RoleA consumes RoleC. RoleB also consumes RoleC. RoleC has a baz() method.

Then 'use Foo' gives an error:

Due to method name conflicts in RoleA and RoleB, the method 'baz' must be included or excluded in Foo…

However, this is no problem in Moose, and I think Moose's behaviour is correct.

See https://gist.github.com/762933 for the actual files used in this test.

@Ovid: It seems to work; thank you for your fast response!

I also vote for "just roles". I've always got Moose/Mouse/Moo if it need them.

Leave a comment

About Ovid

user-pic Have Perl; Will Travel. Freelance Perl/Testing/Agile consultant. Photo by http://www.circle23.com/. Warning: that site is not safe for work. The photographer is a good friend of mine, though, and it's appropriate to credit his work.