Role::Basic - When you only want roles
A long time ago I posted about Roles without Moose and while I still feel that for most cases Moose is the way to go, there can still be a bit of resistance to the idea. Matt Trout responded to my post with how one could have just roles (read his entire post to understand the context):
package Foo::Manual;
use Moose;
extends 'UNIVERSAL'; # get rid of Moose::Object
with 'Foo::Manual::Bar';
sub new { bless {} => shift }
This still involves putting Moose on your servers and when you're faced with a large dev team that is very conservative in their approach, this might be an uphill battle. So what are my alternatives?
You could grab Mouse and rename Mouse::Tiny and get roles, but one of the first questions I was asked was "wasn't that abandoned?" Well, it's been picked up again. And you still have a whole bunch of other things in there which make ultra-conservative devs twitch.
So I was pretty happy to see Role::Tiny released with Moo. The idea was that you could have just roles, in a single package, and nothing else. This makes it easier convince others to dip their toes in the water and yank them out again if they're not comfortable. Of course, at the end of the day, it's not a crime to want roles without all of the extra bits, no matter how shiny those bits may be.
Alas, Role::Tiny had a couple of issues (not the fault of the author). First, here's how you use more than one role:
with 'Some::Role1';
with 'Some::Role2';
Thus, you lose compositional safety and these are not much better than mixins. When I asked Matt Trout about this on Twitter, Frew Schmidt replied:
@OvidPerl @shadowcat_mst when I asked him on IRC a week or so ago he said write tests and he'd do it. So I guess tag your it :-)
That made me very happy and I was going to start, but then I noticed a couple of things. First, Role::Tiny is bundled with Moo. The more modules I have to convince someone to install, the harder it will be to get them installed. Perhaps Role::Tiny will have its own distribution in the future, but then I noticed that it has the before, around and after method modifiers. This is problematic to me.
I've seen people (myself included) using these method modifiers rather casually. Unfortunately, if these modifiers have any side-effects then the order in which they are composed becomes important. For example, what does the following code print?
#!/usr/bin/env perl
{
package Some::Role;
use Moose::Role;
requires qw(some_method);
before some_method => sub {
my $self = shift;
$self->some_number( $self->some_number + 2 );
};
}
{
package Another::Role;
use Moose::Role;
requires qw(some_method);
before some_method => sub {
my $self = shift;
$self->some_number( $self->some_number / 2 );
};
}
{
package Some::Class;
use Moose;
my @roles =
int( rand(2) )
? qw(Another::Role Some::Role)
: qw(Some::Role Another::Role);
with @roles;
has some_number => ( is => 'rw', isa => 'Num' );
sub some_method { print shift->some_number, $/ }
}
my $o = Some::Class->new( { some_number => 7 } );
$o->some_method;
That will print either '4.5' or '5.5', depending on the order in which the roles are consumed. This violates part of the original intent of roles (traits) to be a declarative way of safely decoupling class responsibility and shared behavior. One of the issues with inheritance that roles were designed to fix is the ordering issue (MRO shouldn't be a problem) and I was concerned that introducing method modifiers to devs new to roles might open up a new can of worms. An experienced developer should remember "modifiers should not modify state" (you're forgiven if that's confusing), but the more "rules of thumb" you have to remember, the more "rules of thumb" you'll forget.
Thus, Role::Basic is now on github (and will likely go to the CPAN soon). Here's the core idea:
Here's how to use it.
In a role:
package Does::Serialize::AsYAML;
use Role::Basic;
use YAML::Syck;
requires 'as_hash';
sub serialize {
my $self = shift;
return Dump( $self->as_hash );
}
1;
In your class:
package My::Class;
use Role::Basic 'with';
with qw(
Does::Serialize::AsYAML
);
sub as_hash { ... } # because the role requires it
Nothing fancy. Nothing strange. It's also designed to have a Moose-like syntax to make it easy to migrate to Moose later. Aside from the Moose-like syntax, the primary design goals are safety and simplicity, while still holding true to the spirit of roles. Here's what's come out of this:
- Basic role support including composing into your class, composing roles from other roles, roles declaring requirements, and conflict resolution via aliasing and exclusion.
- Moose-like syntax
- No handling of the SUPER:: bug
- Composition safety (you can't call with() more than once)
- Override safety (optional support for noticing when a class silently ignores a role method)
- No instance application (this might change)
- No method modifiers
If you need something which this module does not provide, you are strongly encouraged to look at Moose or other options.
Hi,
good stuff.
I understand your argument about method modifiers changing state, but I think you are making a overly-strict recommendation.
If a role adds his own state plus method modifiers to trigger his own state modifications, that is a pretty safe thing to do. Wouldn't you agree?
@Pedro: yes, that seems fairly safe and frankly, there's very little in Role::Basic which one can't find a counter-argument against. That's why I strongly urge developers to look at Moose if they want something more. I want Role::Basic to be something simple, safe, and straightforward.
What about Class::Trait? Why a new module?
@Aristotle: as the maintainer of that module and as someone who's long-deprecated it in favor of Moose::Role, I thought a lot about this decision. Class::Trait was created back when roles were still pretty new and folks weren't quite certain what we were going to do with them. It has a fair amount of internal cruft and a fairly ugly syntax. It uses inheritance to set up its behavior, has ugly workarounds for the SUPER:: bug, allows role application at runtime, provides overloading support, and basically has a fair amount of cruft to support marginal features.
With Role::Basic, you get a working subset of Moose::Role behavior with identical syntax (I think this is very important). Up or down migration is easy. All of this is provided in just a couple of hundred lines of ease to follow (and maintain!) code.
Thanks.
That comparison with Class::Trait, and also MSCHWERN/mixin, belongs into the documentation. It should make clear to the reader under which circumstances which module should be preferred.
I'm still failing to grasp why you feel that the order issue illustrated is a show-stopper. Sure it is a UI issue as it 'hides' what is really going on but you are dealing with events that happen linearly in time. If you named these to better map to there action then it would be obvious what is going on. You then exaggerate the issue by randomizing the order, completely obscuring any expectations that could be had at the time that some_method is called.
I can completely understand that providing a toolkit that only gives green dev's enough rope to make a mess and not a noose is a good thing, at times. But to say that you can't have any rope because you could hurt your self seems to take things a bit far. Failure is how we learn these little things.
Will it be safe to use it for (hash based) classes later extended with MooseX::NonMoose?
@zby: it should be safe as it's completely agnostic as to your implementation, but I can't comment on MooseX::NonMoose.
@notbenh: roles are designed to allow one to declaratively assign behavior to a class. Part of the issue with MI and mixins is the procedural nature of their behavior. I'm not saying that declarative code is necessarily better than procedural code, but when the procedural code introduces silent action-at-a-distance and we have hard-to-debug problems, I'll take the declarative code. If people want more, they can upgrade to Moose or Mouse.
Role::Basic is very neat, and I'm using it for a rather lightweight role-based plugin system - see http://search.cpan.org/dist/Brickyard/ .
There's one thing I've run into:
isvalid_method() won't apply imported methods, but if you, in a role, generate accessors using helpers like
then they won't be applied because they are 'imported'. Any idea how to go about this, apart from manually coding the accessors in the role?
@Marcel: I'll have to give this some thought. The problem I see is that so many methods like this can be auto-generated via so many different modules that I'm unsure of how to actually handle that case. Maybe a very minimal:
has 'some_method' => ( is => '"rw");
The problem is that it will have to conform to Moose syntax due to the upgrade path, but that will suggest adding far more behavior than I was anticipating. Suggestions welcome.