Sunday, July 12, 2009

Thoughts on Parameterized Roles

I was discussing parameterized roles with Sartak and doy at YAPC::NA this year. Sartak is the author of the very cool MooseX::Role::Parameterized module, which implements pretty much unlimited parameterization abilities for roles. The shear, unbridled flexibility embodied in that module is insane, which, of course, is both really cool and really scary at the same time. One of our discussion points was about how so much flexibility, if misused, pretty much destroys the benefits of allomorphism you get from roles. With enough parameterization the statement $object->does(SomeRole) has very little meaning anymore since SomeRole could easily be parameterized so that two instances of it do wildly different things. One of the thoughts discussed for solving this problem was to create a stricter set of different kinds of parameters that are allowed. Essentially restricting the functionality to a sane subset through which we can provide some level of guaranteed allomorphism.  While we pretty much rejected that idea for MooseX::Role::Parameterized, the idea stuck in my head.

So the other day on #moose, I was discussing parameterized roles again with Sartak and doy and I mentioned how I have always seen parameterized roles as being very close to ML Functors. The ML family of languages (Standard ML , OCaml, etc.) has an extremely powerful module system which not only has modules (structure in SML) and module signatures (the "type" of the module) but also functors. Functors are best described as modules which take another module as an argument and produce a third module as a result. The book "ML for the Working Programmer" (highly recommended, it is a great book) shows the following conceptual mapping to try and help describe the ML module system. 

  structure ~ value
  signature ~ type
    functor ~ function

But as the book says, this is a helpful starting point, but it fails to convey the full possibilities of the ML module system. 

So at one point in this discussion I decided to try and sketch out how Functor-esque parameterized roles might look and I came up with this (using MooseX::Declare inspired pseudo-code).

role ORDERING { requires 'compare' }

role Sortable [Ordering => (does => 'ORDERING') ] {
    sub sort {
        my ($self, @elements)
        sort { $self->compare($a, $b) } @elements
    }
}

role StringOrder with ORDERING {
    sub compare {
        my (undef, $x, $y) = @_;
        $x cmp $y;
    }
}

role NumericOrder with ORDERING {
    sub compare {
        my (undef, $x, $y) = @_;
        $x <=> $y;
    }
}

role AlphabeticalOrder with ORDERING {
    sub compare {
        my (undef, $x, $y) = @_;
        lc($x) cmp lc($y);
    }
}

class BunchOfStrings with Sortable(StringOrder) {
    # ...
}

class BunchOfNumbers with Sortable(NumericOrder) {
    # ...
}

The first role ORDERING is just an role that requires the compare method and nothing more (an interface), which maps to the ML idea of a signature. 

The second is the parameterized role Sortable which implements a sort method and expects a single role parameter Ordering which must be a role that does the ORDERING interface. This role maps to the ML idea of a Functor. If you notice the Sortable::sort method calls a compare method, which is a method of the ORDERING interface role. The idea here is that the role provided in the parameter Ordering will get composed into the Sortable role and provide the expected compare method. 

The next three roles are just examples of roles that do the ORDERING interface role. Basically one for each of the most common Perl sorting behaviors (at least the most common in my experience). These are pretty simple and straightforward, nothing special here.

After this is a few classes that show how this mechanism might get used. The Sortable(StringOrder) syntax shows the passing of the role parameter (in this case StringOrder) to the parameterized role Sortable. The result of this will produce a third role which is then composed into the BunchOfStrings class.

So, while this is much more restrictive then MooseX::Role::Parameterized, it is much more flexible then simply creating a restricted subset of parameterizable bits. It also (perhaps) solves the allomorphism issue since the "name" of the Sortable(StringOrder) role is simply Sortable(StringOrder) and this clearly provides a predictable and repeatable set of functionality.

So anyway, I do not currently have the tuits to implement this and honestly I kind of want to let this stew for a little longer. It would not replace MooseX::Role::Parameterized but perhaps be called MooseX::Role::Functors or something and can be just another way to do it.

4 comments:

  1. Nice post stevan. It covers a lot of the ground I wanted to. I do have one big point I'd like to share:

    My idea for maintaining complete allomorphism in the face of the ultimate flexibility offered by MooseX::Role::Parameterized is discipline. You may only consume parameterized roles in ordinary roles. That way, you explicitly name each parameterization so that ->does will not lead you astray. Of course, this sort of discipline will not be mandated by MXRP, but it could be a nice middle ground.

    I also liked your idea about ->does('Serialization', { format => 'YAML' }). I think smart match would be a good fit for querying the role's parameters.

    ReplyDelete
  2. Sartak: Whoops, yes I totally forgot to mentioned both the discipline and ->does('Serialization', { format => 'YAML' }) ideas, both excellent points.

    ReplyDelete
  3. Screw allomorphism!

    Seriously, I've been using MX::R::P as a kind of macro system, and it's worked really nicely for that. I think it's very useful for this sort of thing, though it would make more sense if it wasn't even called a role.

    ReplyDelete
  4. I really like this, and I second the notion that MXRP doesn't really implement roles; it's more of a role factory. (Sartak's suggestion about consuming parameterized roles only in other non-parameterized roles fits nicely in with this.)

    ReplyDelete