MOPing with Moose

I’ve used Moose regularly now for a couple years, but really only so far as most people do, as a handy accessor generator and type checker. But my most recent project has called on me to delve, first into Roles, which are a lovely way to teach old classes new tricks, but I found I wanted to do deeper magic…

Now, as I wrote about before, my project is a Node.js style event system for Moose. Moose classes have two types of things associated with them, attributes and methods.

I allow users to declare that their class will emit a particular event through a helper method. You just write:

has_event 'connected';

And now users can register a listener to be called when you emit the ‘connected’ event. Conceptually, these events are really a third type of thing associated with the class— they should inherit the way methods and attributes, but otherwise have no further meta data associated with them. They either exist or they don’t. So at first I thought perhaps metaclass traits would be helpful. But they don’t inherit, which is a key feature I need. If you subclass an event bearing class, your class is going to emit the same events it does (and maybe more).

Finally what I settled on was having the helper add specially named methods to the class. If the method exists, then the event exists and we emit it as usual. Otherwise we don’t. My has_event helper now looks like this:

my $stub = sub {};
sub has_event {
    my $class = caller();
    $class->meta->add_method( "event:$_" => $stub ) for @_;
}

With that settled, I had to export my helper, which was easy enough to do with Mo[ou]se::Exporter. This also allows me to reduce the amount of boiler plate in my class. Now, since my helper needs meta data, I would use with_meta and grab it from my @_, except that Mouse::Exporter doesn’t support with_meta =/. And so I do it the above way instead.

Also in the vein of reducing boiler plate, if you have to “use” my package to get my helpers, I didn’t want you to also have to “with” my Role. My impression had been that the base_class_roles argument to setup_import_methods would specify roles that should be added to the class that used my package, but this doesn’t seem to work for me. As such, I started with my init_meta calling $role_class->meta->apply( $importer->meta ) by hand. This, however, made it impossible for people to alias or exclude methods from my Role. So finally I created my own import that slurped in any -alias or -exclude arguments and passed them on to apply, sending the rest to the default import. Less elegant then I would like, but it does get the job done.

As a result, you can call:

package My::Class {
    use MooseX::Event -alias=>{ on => 'listen_on' };
    has_event 'foo';
    sub on { # Just an example of using a method with the same name as a method
                  # from the event role
       my $self = shift;
       $self->emit('foo');
    }
}
my $example = My::Class->new();
$example->listen_on( foo => sub { say "Hi" } );
$example->on(); # prints "Hi"

2 Comments

It's entirely possible to add new types of meta-information to a metaclass. My Fey::ORM module provides two metaclasses (Fey::Meta::{Table,Schema}) that do this.

I'll probably rewrite those classes as traits, but the basic idea remains the same.

Leave a comment

About Rebecca

user-pic I've been happily playing in Perl's playground since 1997, when I decided I really wanted to work with Unix and the Web, rather then DOS, where my previous work had been. Lately, my rate of publishing has code has increased a lot, in large part thanks to Github.