May 12, 2009

The Design of Parameterized Roles

There has been a lot of good discussion about roles lately, due to chromatic and Ovid. We're starting to attract people from outside of not only the Moose and Perl 6 communities, but outside of the greater Perl community. I'm thrilled, because roles are an excellent complement to inheritance, and deserve to be widely adopted.

In The Why of Perl Roles, chromatic quickly describes parametric roles, also known as parameterized roles. A parameterized role is a role whose application can be customized. Such a role accepts parameters at composition time to alter its behavior any way it wishes. This increases the flexibility of the role, fostering opportunities for greater code reuse.

Example

My NetHack bot TAEB has a TAEB::Action::Role::Item parameterized role. It accepts one parameter, a list of names for item attributes. These attributes (which specify a type constraint of NetHack::Item and a custom "provided" trait) are added to the class upon composition. The role provides a few other things as well: a current_item attribute and a method for dealing with missing-item exceptions. The current_item attribute's value is a lazy default, pointing to the first item in the parameterized list of attribute names. This is an application of the well-trodden Don't Repeat Yourself principle — each class does not have to repeat its item attribute names. Parameterized roles are pretty good enablers of DRY!

Since NetHack is a very item-oriented game, this role has many consumers. The TAEB::Action::Dip consumer specifies item and into for the list of attribute names, so it gets those two attributes (as well as whatever else the role provides). Likewise, TAEB::Action::Wield gets a weapon attribute. Most of the consumers specify nothing for this parameter, so they get the default list of item. Consuming a parameterized role without specifying any parameters is syntactically identical to applying vanilla roles, which is a (minor) convenience.

In older versions of the code, TAEB::Action::Role::Item was not parameterized. This role always gave you a single attribute named item. The into attribute of TAEB::Action::Dip was a second-class citizen. There are a number of ways a second-class item attribute can go wrong, such as not having a type constraint, or not being visible to current_item's lazy default. It also prevented us from renaming the item attribute to, say, weapon for TAEB::Action::Wield. We drastically improved the code's clarity and expressiveness by parameterizing the role.

Design

In December, I wrote the MooseX::Role::Parameterized extension. I had grown tired of people (primarily myself!) putting off coding because the proper implementation of whatever demanded parameterized roles. I am not exaggerating:

  • "hoping once we have parameterised roles we can do that"
  • "you will have [...] parameterized roles which will allow such things very easily"
  • "let's see what the parameterized roles end up giving us"
  • "once we have parameterised roles I shall be providing one to produce all three trivially"
  • "parameterized roles will probably do all that you want"
  • "I'm hoping parameterised roles will let me get rid of that"
  • "what's the status of parameterized roles? I could really use them"
  • "has there been any progress on parameterized roles?"
  • "we don't have parameterized roles yet?!"
  • "are parametrized roles somewhere on the roadmap"
  • "that sounds like parameterized roles, which someone should implement some time"

I think the biggest roadblock in the way of implementing a parameterized role system was Moose's lack of support for anonymous roles. So I did that hard part first then wrote MooseX::Role::Parameterized.

Designers of parameterized role systems (perhaps three or four of us by now?) must decide how much freedom each role is given. How much parameterizability is permissible? In MooseX::Role::Parameterized, and Perl 6's parametric roles, there are no limitations. You can parameterize anything.

Maximizing your user's freedom is certainly laudable, but I am not convinced that it is completely beneficial. There is value in stricture, for example, to improve the efficacy of class/role browsers. I will leave such meditations for the next parameterized role designer. At least for Moose, we have several more chances to get the design right; there are plenty of other namespaces available on CPAN: MooseX::Role::Parametric, MooseX::Role::Parameterizable, MooseX::Role::Parameterised, and so on.

MooseX::Role::Parameterized works, in short, by executing a procedure for each role application. This procedure takes as an argument a "parameters" object. It is executed in the dynamic extent of an anonymous role metaobject. All of the usual role keywords (has, around, requires, etc) operate on this anonymous role. The procedure may use the "parameters" object in building up the role using these keywords.

This is also a reasonable description of how vanilla roles are created, from a backstage point-of-view. The primary difference is that the procedure is only ever executed once for vanilla roles, whereas it is executed potentially many times, building up a different role metaobject each time, for parameterized roles.

This difference is precisely what makes the syntax of MooseX::Role::Parameterized slightly unusual, at least in the context of core Moose:

package Counter;
use MooseX::Role::Parameterized;

parameter name => (
    isa      => 'Str',
    required => 1,
);

You explicitly specify which parameters the role can take. This improves introspectability, and makes it immediately obvious (if you've written Moose code) how to specify that a parameter is required, or has a type constraint; default value; or predicate, etc.

role {
    my $p = shift;
    my $name = $p->name;

    has $name => (
        is  => 'rw',
        isa => 'Int',
    );

    method "increment_$name" => sub {
        my $self = shift;
        $self->$name($self->$name + 1);
    };
};

The role keyword exists for specifying the aforementioned role-building procedure. This is the unusual syntax. MooseX::Declare could improve upon this a lot, once some tuits are spent.

The method keyword is required (instead of the usual sub name { ... }) because Perl handles named, inner subroutines in a way that is not useful for this design. The details are many, but mostly unimportant. The salient point is that such subroutines do not properly close over $p, rendering them unparameterizable. The anonymous subroutine passed to method can properly capture $p. The method keyword has a fringe benefit over sub in easing the parameterization of the method's name.

Current and future work

I recently fixed a bug in MooseX::Role::Parameterized that prevented parameterized roles from participating in role summation. Role summation is what provides protection against method name conflicts at compile time, among other things. Being unable to partake in composition limited parameterized roles' usefulness. I'm very happy to have fixed the one crippling issue with this extension.

The fix involved a bait-and-switch hook in Moose's role summation code. The hook is fairly contrived, but it might possibly be useful to other role extensions.

I'm currently noodling around with the partial application of parameterized roles. Currently, if a role consumes a parameterized role, it must specify all of the required parameters. In fact, the parameterized role is bound (the procedure is executed) at that moment. MooseX::Role::Parameterized does not care about whether it is being consumed by a class or a role. Instead, I'd like the consuming role to be able to specify some of the parameters. Any unbound parameters will be parameters in the composite role.

The only blocker stopping me from working on partial application of parameterized roles is a use case. Adding complexity for no reason is clumsy.