Moose!

Ovid on 2009-02-20T15:04:26

We've started to use Moose pretty heavily at work, but until today, I've not had the chance to write any Moose classes myself. Today, I wrote my first two Moose classes:

package PIPs::QueryParams::Param;

use Moose;

has name => (is => 'ro', isa => 'Str', required => 1);

has method =>
        (is => 'ro', isa => 'Str', lazy => 1, default => sub { $_[0]->name });
has required   => (is => 'ro', isa => 'Bool', default => 0);
has repeatable => (is => 'ro', isa => 'Bool', default => 0);
has datatype   => (is => 'ro', isa => 'Str',  default => 'xsd:string');
has rel        => (is => 'ro', isa => 'Str',  default => '');

__PACKAGE__->meta->make_immutable;
no Moose;

1;

And ...

package PIPs::QueryParams;

use Moose;
use Carp ();
use PIPs::QueryParams::Param;

has params => (
    is  => 'ro',
    isa => 'ArrayRef[PIPs::QueryParams::Param]',
);

sub BUILDARGS {
    my ($class, %args) = @_;
    my @params;
    my $controller           = $args{controller};
    my $allowed_query_params = $controller->allowed_query_params;
    foreach my $name ( sort keys  %$allowed_query_params) {
        my $args = $allowed_query_params->{$name};
        $args->{name} = $name;
        push @params => PIPs::QueryParams::Param->new(%$args);
    }
    $class->SUPER::BUILDARGS(
        params => \@params,
    );
}

__PACKAGE__->meta->make_immutable;
no Moose;

1;

And I can now just do this:

my $params = PIPs::QueryParams->new( controller => $controller );
my $list   = $params->params;
say $_->name foreach @$list;

I'm sure those can be written better, but those were soooo much easier to write than typical Perl classes. Ultimately, this is going to be used to auto-generate documentation for our REST API.

And note, because of the annoying sort of guy that I am (and preferring immutable objects), everything is read-only.

Update: Fixed the code per autarch's suggestion below. His comment now won't make much sense, but I didn't want an example of bad Moose code here.


Curious...

Adrian on 2009-02-20T15:27:12

.... why the trailing "no Moose"?

Re:Curious...

Ovid on 2009-02-20T15:37:17

It removed the functions that Moose exports into your package. It would be annoying if someone tried to call $object->has :)

Re:Curious...

Ovid on 2009-02-20T15:39:42

The lovely MooseX::Declare lets you avoid even that (and it's an incredible module), but unfortunately, it's very alpha and has plenty of bugs. Still, look what it buys you:

use MooseX::Declare;

class BankAccount {
    has 'balance' => ( isa => 'Num', is => 'rw', default => 0 );

    method deposit (Num $amount) {
        $self->balance( $self->balance + $amount );
    }

    method withdraw (Num $amount) {
        my $current_balance = $self->balance();
        ( $current_balance >= $amount )
            || confess "Account overdrawn";
        $self->balance( $current_balance - $amount );
    }
}

I can't wait until that's production ready.

Re:Curious...

Adrian on 2009-02-20T15:44:17

> I can't wait until that's production ready.

Ohhh. Shiny! :-)

Re:Curious...

rafl on 2009-02-20T18:31:10

All MooseX::Declare does to not require you to unimport all Moose keywords (and possibly other imports) is

use namespace::clean -except => 'meta';

to remove all previously defined and imported functions (except for the 'meta' method Moose gives you) at the end of compile time. No need to wait for anything.

Re:Curious...

draegtun on 2009-02-20T23:04:48

I can't wait until that's production ready.

I think everyone who left the London Perl tech talk last night felt & thought the exactly same thing.

Patience is a virtue!

/I3az/

What about ''?

autarch on 2009-02-20T16:29:31

I don't understand the bit about the final '' in your comment. I think a better way to do that is make method lazy and have the default be:

sub { $_[0]->name }

Re:What about ''?

Ovid on 2009-02-20T16:51:52

The reason for the final '' is that it put a string in the method, thus ensuring it would pass type validation. Your solution is much nicer. Thanks :)

Ohh, Moose Golf!

Stevan on 2009-02-20T18:36:41

So, in the interest of TIMTOWTDI, here is another approach that uses coercion to extract the params list from the controller object.

package PIPs::QueryParams;
use Moose;
use Moose::Util::TypeContraints;
use PIPs::QueryParams::Param;

# I am assuming this is the name
# of your controller class and
# it is a non-Moose class, if it is
# a Moose class, you can remove this
# (assuming the class is already loaded)
# because Moose does this for you
class_type 'PIPs::Controller';

subtype 'PIPs::ListOfParams'
    => as 'ArrayRef[PIPs::QueryParams::Param]';

coerce 'PIPs::ListOfParams'
    => from 'PIPs::Controller'
    => via {
        my $controller = $_;
        my @params;
        my $allowed_query_params = $controller->allowed_query_params;
        foreach my $name ( sort keys  %$allowed_query_params) {
            my $args = $allowed_query_params->{$name};
            $args->{name} = $name;
            push @params => PIPs::QueryParams::Param->new(%$args);
        }
        return \@params;
    };

has params => (
    is     => 'ro',
    isa    => 'PIPs::ListOfParams',
    coerce => 1,
);

__PACKAGE__->meta->make_immutable;

no Moose; 1;

And then your code would look like this ...

my $params = PIPs::QueryParams->new( params => $controller );
my $list   = $params->params;
say $_->name foreach @$list;

Now, passing a controller where your supposed to pass params does seem a little odd, so perhaps this isnt the best example. However you could just as easily pass in the $controller->allowed_query_params and do the coercion on the HashRef[HashRef] type (or better yet, use MooseX::Types::Structured to define a stricter Dictionary type).

One of the benefits I see with using types and coercion is that since types are global, this promotes cross cutting re-use since now any class that needs a list of PIPs::QueryParams::Param objects and happens to have easy access to a $controller object can use this coercion.

Anyway, glad your enjoying the Moose :)

- Stevan

A code smell

autarch on 2009-02-20T21:54:21

Thinking about this example, there's a code smell in general with your use of BUILDARGS.

BUILDARGS is really _not_ for munging arguments, it's there to allow for a constructor which doesn't take named argument, like

my $user = User->new($user_id);

If you're using it to add extra arguments, consider using lazy default/builder, per my suggestion. If you're using it to coerce, use a coercion per Stevan's example.

Re:A code smell

Ovid on 2009-02-20T22:32:05

OK, I'll buy the code smell argument because you have more experience with this than I do, but I don't understand. I like the lazy/default example, but Stevan's example is longer than mine and doesn't seem to buy me anything. Plus, as Stevan pointed out, it has the drawback of an apparently misnamed constructor pair. What am I missing?

Re:A code smell

autarch on 2009-02-20T23:27:34

The advantage of coercion is exactly what Stevan says, it lets you reuse that bit of logic across classes.

The name mismatch is weird, but I'd probably just have people call the constructor like this:

PIPs::QueryParams->new( params => $controller->allowed_query_params );

I honestly don't like _any_ of these options much myself, but I don't understand your APIs or problem domain well enough to come up with a better solution.

There's just something about using BUILDARGS to do what coercion can do that strikes me as weird.