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:

Role::Basic and Moose::Role

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.

11 Comments

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?

What about Class::Trait? Why a new module?

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?

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

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

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?

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.