Introduction
If you’re waiting for the Corinna OOP project to land in the Perl core, you’ll have to wait. Paul Evans will start working on the implementation after Perl version 5.36 is released. However, it’s a lot of work and it’s not clear that if it will be ready by 5.38 and even then, it will be experimental. You’ve a couple of years to wait.
To bridge the gap, I’ve released MooseX::Extended . It’s not really “Corinna-lite” because the way Moose works and the way Corinna works aren’t quite compatible, but it will make your Moose code easier to write.
Less Boilerplate
The bane of many programmer’s existence is boilerplate. We hate boilerplate. Not only do we have to type the boilerplate, but we also have to remember it. For a simple, “read only” 2D point class in Moose, the class and my boilerplate looks something like this:
package My::Point::Moose;
use v5.20.0;
use Moose;
use MooseX::StrictConstructor;
use Types::Standard qw(Num);
use feature qw( signatures postderef postderef_qq );
no warnings qw( experimental::signatures experimental::postderef );
use namespace::autoclean;
use mro 'c3';
has [ 'x', 'y' ] => ( is => 'ro', isa => Num );
__PACKAGE__->meta->make_immutable;
1;
That’s a lot of boilerplate. Some of it’s not needed for this module, so I am often lazy and don’t write the boilerplate I don’t need, meaning I have to go back and add it later as the class grows.
Here’s the same code with MooseX::Extended
:
package My::Point;
use MooseX::Extended types => [qw/Num/];
param [ 'x', 'y' ] => ( isa => Num );
That’s right, you don’t have to declare it immutable or end it with a true
value. MooseX::Extended
handles that for you (though you can disable this).
Hmm, what if I want to subclass that and make it mutable?
package My::Point::Mutable::Moose;
use v5.20.0;
use Moose;
extends 'My::Point::Moose';
use MooseX::StrictConstructor;
use feature qw( signatures postderef postderef_qq );
no warnings qw( experimental::signatures experimental::postderef );
use namespace::autoclean;
use mro 'c3';
has '+x' => (
is => 'ro',
writer => 'set_x',
clearer => 'clear_x',
default => 0,
);
has '+y' => (
is => 'ro',
writer => 'set_y',
clearer => 'clear_y',
default => 0,
);
sub invert ($self) {
my ( $x, $y ) = ( $self->x, $self->y );
$self->set_x($y);
$self->set_y($x);
}
__PACKAGE__->meta->make_immutable;
1;
Here it is in MooseX::Extended
:
package My::Point::Mutable;
use MooseX::Extended;
extends 'My::Point';
param [ '+x', '+y' ] => ( writer => 1, clearer => 1, default => 0 );
sub invert ($self) {
my ( $x, $y ) = ( $self->x, $self->y );
$self->set_x($y);
$self->set_y($x);
}
Again, it lets me focus on writing code that works, rather than all the scaffolding that’s usually needed.
Note: we also provide MooseX::Extended::Role which behaves similarly.
param and field
You can still use the has
function to declare your Moose attributes, but I
recommend using param
and field
instead. Why? We’ve regularly face the
following problem:
package Some::Class;
use Moose;
has name => (...);
has uuid => (...);
has id => (...);
has backlog => (...);
has auth => (...);
has username => (...);
has password => (...);
has cache => (...);
has this => (...);
has that => (...);
Which of those should be passed to the constructor and which should not? Just because you can pass something to the constructor doesn’t mean you should. Unfortunately, Moose defaults to “opt-out” rather than “opt-in” for constructor arguments. This makes it really easy to build objects, but means that you can pass unexpected things to the constructor and it won’t always work the way you want it to.
There’s an arcane init_arg => undef
pair to pass to each to say “this
cannot be set via the constructor,” but many developers are either unaware of
this is simply forget about it. MooseX::Extended
solves with by separating
has
into param
(allowed in the constructor, but you can also use
default
or builder
) and field
, which is forbidden in the constructor.
We can rewrite the above as this:
package Some::Class;
use MooseX::Extended;
param name => (...);
param backlog => (...);
param auth => (...);
param username => (...);
param password => (...);
field cache => (...);
field this => (...);
field that => (...);
field uuid => (...);
field id => (...);
And now you can instantly see what is and is not intended to be allowed in the constructor.
There’s a lot more I can say about that, but you can read the manual for more information.
Attribute Shortcuts
When using field
or param
, we have some attribute shortcuts:
param name => (
isa => NonEmptyStr,
writer => 1, # set_name
reader => 1, # get_name
predicate => 1, # has_name
clearer => 1, # clear_name
builder => 1, # _build_name
);
sub _build_name ($self) {
...
}
These can also be used when you pass an array reference to the function:
package Point {
use MooseX::Extended types => ['Int'];
param [ 'x', 'y' ] => (
isa => Int,
clearer => 1, # clear_x and clear_y available
default => 0,
);
}
Note that these are shortcuts and they make attributes easier to write and more consistent. However, you can still use full names:
field authz_delegate => (
builder => '_build_my_darned_authz_delegate',
);
Cloning
MooseX::Extended
offers optional, EXPERIMENTAL support for attribute
cloning, but differently from how we see it typically done. You can just pass
the clone => 1
argument to your attribute and it will be clone with
Storable
‘s dclone
function every time you read or write that attribute,
it will be cloned if it’s a reference, ensuring that your object is
effectively immutable.
If you prefer, you can also pass a code reference or the name of a method you
will use to clone the object. Each will receive three arguments:
$self, $attribute_name, $value_to_clone
. Here’s a full example, taken
from our test suite.
package My::Event;
use MooseX::Extended types => [qw(NonEmptyStr HashRef InstanceOf)];
param name => ( isa => NonEmptyStr );
param payload => (
isa => HashRef,
clone => 1, # uses Storable::dclone
writer => 1,
);
param start_date => (
isa => InstanceOf ['DateTime'],
clone => sub ( $self, $name, $value ) {
return $value->clone;
},
);
param end_date => (
isa => InstanceOf ['DateTime'],
clone => '_clone_end_date',
);
sub _clone_end_date ( $self, $name, $value ) {
return $value->clone;
}
sub BUILD ( $self, @ ) {
if ( $self->end_date < $self->start_date ) {
croak("End date must not be before start date");
}
}
Customization
Not everyone likes MooseX::StrictConstructor
. It’s useful to prevent this
error:
my $name = My::Name->new( naem => 'Ovid' );
In bog-standard Moose, the misspelled attribute will be discarded, leading to bugs that can be hard to track down. However, some code (such as DBIx::Class ), breaks with a strict constructor. You can skip that:
use MooseX::Extended excludes => [qw/StrictConstructor/];
Both MooseX::Extended
and MooseX::Extended::Role
have a variety of features
you can exclude if they don’t work for you. Again, see the documentation.
Note that at the present time, this customization is per class and cannot be inherited. This may change in a future release.
Conclusion
There’s a lot more packed into MooseX::Extended
, but this covers the
highlights. I think MooseX::Extended
will make many Perl programmer’s life
easier if they use Moose, but want a better set of defaults. It’s still in
ALPHA, but it' close to a feature-complete BETA release. I think it’s
relatively solid, but no guarantees. The github
repo has Linux and Windows CI workflows
set up to catch errors quickly and the test suite is coming along nicely.
If you’re interested in starting any new projects using Moose, please give
MooseX::Extended
a try and let me know what you think.