Building your own Moose

So you've switched to Moose a long time ago and you're quite happy with it, but you slowly notice that there are, well, things you wish it did differently. Fortunately, there's the MooseX:: namespace and now you have boilerplate you type at the beginning of every module.

use Moose;
use MooseX::StrictConstructor;
use MooseX::HasDefaults::RO;
use My::Moose::Types;

The above incantation says "die if there are unknown arguments to new(), and attributes should be read-only by default, and throw in my custom-defined Moose types as well".

But boilerplate is bad. In my quest to remove boilerplate, I've produced Test::Most and other modules, but in this case, there's not much to release because many people's Moose preferences will be different. Still, it would be much nicer to write use My::Moose; rather than continually repeating the lines above, so here's how to do that.

Let's take a look at Mason::Moose for inspiration. The core code looks like this:

package Mason::Moose;
BEGIN {
  $Mason::Moose::VERSION = '2.20';
}    ## no critic (Moose::RequireMakeImmutable)
use Moose                      ();
use MooseX::HasDefaults::RO    ();
use MooseX::StrictConstructor  ();
use Method::Signatures::Simple ();
use Moose::Exporter;
use strict;
use warnings;
Moose::Exporter->setup_import_methods( also => ['Moose'] );

sub init_meta {
    my $class     = shift;
    my %params    = @_;
    my $for_class = $params{for_class};
    Method::Signatures::Simple->import( into => $for_class );
    Moose->init_meta(@_);
    MooseX::StrictConstructor->import( { into => $for_class } );
    MooseX::HasDefaults::RO->import( { into => $for_class } );
    {
        no strict 'refs';
        *{ $for_class . '::CLASS' } = sub () { $for_class };    # like CLASS.pm
    }
}

1;

It's absolutely lovely, but there are a couple of changes we want. First, let's break this down. You probably understand this bit:

use Moose                      ();
use MooseX::HasDefaults::RO    ();
use MooseX::StrictConstructor  ();
use Method::Signatures::Simple ();

By providing an empty list as the argument after a module name you use, you prevent the import method from being called. That's useful because we don't want those module behaviors in My::Moose, we want those behaviors in the modules which use My::Moose;.

Next, we have Moose::Exporter:

use Moose::Exporter;
Moose::Exporter->setup_import_methods( also => ['Moose'] );

As you know, Moose exports many helper functions into your class. Moose::Exporter let's you do the same thing, but you have a huge amount of control over what you want to export. For the code above, the also => ['Moose'] says "export all Moose functions into our calling class."

In this case, we want to do a bit more, so if you provide an init_meta class method, that will be called to let you set up additional behavior:

sub init_meta {
    my $class     = shift;
    my %params    = @_;
    my $for_class = $params{for_class};
    Method::Signatures::Simple->import( into => $for_class );
    Moose->init_meta(@_);
    MooseX::StrictConstructor->import( { into => $for_class } );
    MooseX::HasDefaults::RO->import( { into => $for_class } );
    {
        no strict 'refs';
        *{ $for_class . '::CLASS' } = sub () { $for_class };    # like CLASS.pm
    }
}

The $params{for_class} value is guaranteed to be the name of the calling class. Then, you simply read the documentation of the classes you're playing with to figure out how to implement the behavior you want (note how some of the import methods have subtly different syntax).

In our case, we want something a bit simpler than what's above, but we also have an interesting problem. I often use attribute traits and, as it turns out, they throw lots of nasty warnings with MooseX::HasDefaults::RO, so that's an issue for me, but I still want read-only to be the default behavior of my attributes. So what can I do?

Moose::Exporter's setup_import_methods method also has a with_meta parameter. Any function listed there will be passed the class metaobject as the first argument, with subsequent arguments being whatever was passed to the function by the calling class. Further, if that function matches a name in Moose, it will replace the Moose version of that function. Thus, I can override the has function:

Moose::Exporter->setup_import_methods(
    with_meta => ['has'],
    also      => ['Moose'],
);
sub has {
    my ( $meta, $attribute_name, %options ) = @_;
    $options{is} //= 'ro';
    $meta->add_attribute( $attribute_name, %options );
}

And in your calling class:

use My::Moose;
has 'something' => ( isa => 'Str' ); # it's now read-only

Except that doesn't quite work either, because there are two use cases we didn't quite consider.

First, it's OK to write this:

has \@list_of_attributes => ( is => 'ro', isa => 'Int' );

That means instead of writing this:

has 'foo' => ( is => 'ro', isa => 'Int' );
has 'bar' => ( is => 'ro', isa => 'Int' );
has 'baz' => ( is => 'ro', isa => 'Int' );

You can write this and have it mean the same thing:

has [qw/foo bar baz/] => ( is => 'ro', isa => 'Int' );

Less boilerplate!

However, in our overridden has() function, the add_attribute method is expecting a string as its first argument, not an array ref.

The second, rather less common case, is when you don't an accessor defined. This is useful if you're just creating an attribute for another object and you want to delegate methods to it:

has 'user_agent' => (
    isa        => 'Some::UserAgent',
    handles    => { _request => 'request' },
    lazy_build => 1,
}
sub _build_user_agent { ... }

That generates warnings, so is => 'bare' is recommended instead of omitting is (usually a sign of the programmer just forgetting), so we won't handle this case. That means our has function looks like this:

sub has {
    my ( $meta, $name, %options ) = @_;

    $options{is} //= 'ro';

    # "has [@attributes]" versus "has $attribute"
    foreach ( 'ARRAY' eq ref $name ? @$name : $name ) {
        $meta->add_attribute( $_, %options );
    }
}

So now that has \@attributes example we showed above is simplified even further into three read-only integers:

has [qw/foo bar baz/] => ( isa => 'Int' );

And putting it all together, along with your My::Moose::Types module gets us this:

package My::Moose;

use 5.010; # for //
use Moose                      ();
use MooseX::StrictConstructor  ();
use My::Moose::Types;
use Moose::Exporter;
Moose::Exporter->setup_import_methods(
    with_meta => ['has'],
    also      => ['Moose'],
);

sub init_meta {
    my $class     = shift;
    my %params    = @_;
    my $for_class = $params{for_class};
    Moose->init_meta(@_);
    MooseX::StrictConstructor->import( { into => $for_class } );
}

sub has {
    my ( $meta, $name, %options ) = @_;

    $options{is} //= 'ro';

    # "has [@attributes]" versus "has $attribute"
    foreach ( 'ARRAY' eq ref $name ? @$name : $name ) {
        $meta->add_attribute( $_, %options );
    }
}

1;

And now instead of always trying to remember to type this:

use Moose;
use MooseX::StrictConstructor;
use MooseX::HasDefaults::RO;
use My::Moose::Types;

You can just type this:

use My::Moose;

Leave a comment

About Ovid

user-pic Freelance Perl/Testing/Agile consultant and trainer. See http://www.allaroundtheworld.fr/ for our services. If you have a problem with Perl, we will solve it for you. And don't forget to buy my book! http://www.amazon.com/Beginning-Perl-Curtis-Poe/dp/1118013840/