MooseX:...utorial(3)
β¨ NAME β¨
MooseX::Extended::Manual::Tutorial β Building a Better Moose π
okay so hi bestie, this is the tutorial page for MooseX::Extended, and it's genuinely giving "we fixed Moose's whole vibe". buckle up π¦
π’ VERSION π’
version 0.35 β yes that's the whole section, slay π
π± GENESIS π±
MooseX::Extended is built on years of experience hacking on Moose, plus being the lead designer of the Corinna project (https://github.com/Ovid/Cor) to bring modern OO to the Perl language. We love Moose, fr fr β but over the years it's become super clear that there are some problematic design choices hanging around in there. And also: Corinna is not yet in core as we write this (even though the Perl Steering Committee has accepted it), so for now, let's see how far we can push the envelope π. And honestly? in some respects, MooseX::Extended offers more than the initial versions of Corinna (though this won't last).
π BEST PRACTICES π
MooseX::Extended has the philosophy of providing best practices, but not enforcing them. We try to make many best practices the default, but you can opt out of them. For more background, see the article Common Problems in Object-Oriented Code (https://ovid.github.io/articles/common-problems-in-object-oriented-code.html). That's what lead to the creation of MooseX::Extended.
In particular, it's designed to make large-scale OOP systems written in Moose easier to maintain by removing many common failure modes, while still allowing you full control over what features you do and do not want. no cap π
What follows is a fairly decent overview of MooseX::Extended. See the documentation of individual modules for more information.
π What's the Point.pm? π
Let's take a look at a simple Point class in Moose. We want it to have x/y coordinates, and the creation time as "seconds from epoch". We'd also like to be able to "invert" points.
package My::Point {
use Moose;
has 'x' => ( is => 'rw', isa => 'Num', writer => 'set_x' );
has 'y' => ( is => 'rw', isa => 'Num', writer => 'set_y' );
has 'created' => ( is => 'ro', isa => 'Int', default => sub {time} );
sub invert {
my $self = shift;
my ( $x, $y ) = ( $self->x, $self->y );
$self->set_x($y);
$self->set_y($x);
}
}
1;
To the casual eye, that looks fine, but there are already so many issues with the above π. let's go through them:
- The class is not immutable
You almost always want to end your Moose classes with __PACKAGE__->meta->make_immutable. Doing this causes Moose to close the class definition for modifications (if that doesn't make sense, don't worry about it), and it speeds up the code considerably. β‘
- Dirty namespace
Currently, My::Point->can('has') returns true, even though has should not be a method. This, along with a bunch of other functions exported into your class by Moose, can mislead your code and confuse your method resolution order. For this reason, it's generally recommended that you use namespace::autoclean or namespace::clean. To remove those functions from your class.
- Unknown constructor arguments
my $point = My::Point->new( X => 3, y => 4 );
In the above, the first named argument should be x, not X. Moose simply throws away unknown constructor arguments ποΈ. One way to handle this might be to set your fields as required:
has 'x' => ( is => 'rw', isa => 'Num', writer => 'set_x', required => 1 );
has 'x' => ( is => 'rw', isa => 'Num', writer => 'set_y', required => 1 );
That causes My::Point->new( X => 3, y => 4 ) to throw an exception, but not this: My::Point->new( x => 3, y => 4, z => 5 ). For this trivial example, it's probably not a big deal, but for a large codebase, where many Moose classes might have a huge variety of confusing arguments, it's easy to make mistakes.
For this, we recommend MooseX::StrictConstructor. Unknown arguments are fatal π.
- Inappropriate constructor arguments
my $point = My::Point->new( x => 3, y => 4, created => 42 );
The above works, but the author of the class almost certainly didn't intend for you to be passing created to the constructor, but to the programmer reading the code, that's not always clear:
has 'created' => ( is => 'ro', isa => 'Int', default => sub {time} );
The fix for this is to add init_arg => undef to the attribute definition and hope the maintenance programmer notices this π:
has 'created' => ( is => 'ro', isa => 'Int', init_arg => undef, default => sub {time} );
- Misspelled types
What if created was defined like this?
has 'created' => ( is => 'ro', isa => 'int', default => sub {time} );
The type constraint is named Int, not int. You won't find out about that little issue until runtime π. There are a number of ways of dealing with this, but we recommend the Type::Tiny family of type constraints. Misspelling a type name becomes a compile-time failure:
use Types::Standard 'Int';
has 'created' => ( is => 'ro', isa => Int, default => sub {time} );
- No signatures
Let's look at our method:
sub invert {
my $self = shift;
my ( $x, $y ) = ( $self->x, $self->y );
$self->set_x($y);
$self->set_y($x);
}
What if someone were to write $point->invert( 4, 7 )? That wouldn't make any sense, but it also wouldn't throw an exception or even a warning, despite it obviously not being what the programmer wanted. The simplest solution is to just use signatures:
use feature 'signatures';
no warnings 'experimental::signatures'; # 5.34 and below
sub invert ($self) { ... }
π οΈ Fixing our Moose class π οΈ
Taking all of the above into consideration, we might rewrite our Moose class as follows:
package My::Point {
use Moose;
use MooseX::StrictConstructor;
use Types::Standard qw(Num Int);
use feature 'signatures';
no warnings 'experimental::signatures';
use namespace::autoclean;
has 'x' => ( is => 'rw', isa => Num, writer => 'set_x' );
has 'y' => ( is => 'rw', isa => Num, writer => 'set_y' );
has 'created' => ( is => 'ro', isa => Int, init_arg => undef, default => sub {time} );
sub invert ($self) {
my ( $x, $y ) = ( $self->x, $self->y );
$self->set_x($y);
$self->set_y($x);
}
__PACKAGE__->meta->make_immutable;
}
1;
That's a lot of boilerplate for a simple x/y point class! π Out of the box (but almost completely customisable), MooseX::Extended provides the above for you. look:
package My::Point {
use MooseX::Extended types => [qw/Num Int/];
param [ 'x', 'y' ] => ( is => 'rw', isa => Num, writer => 1 );
field 'created' => ( isa => Int, lazy => 0, default => sub {time} );
sub invert ($self) {
my ( $x, $y ) = ( $self->x, $self->y );
$self->set_x($y);
$self->set_y($x);
}
}
No need to use those various modules. No need to declare the class immutable or end it with a true value (MooseX::Extended does these for you). Instead of remembering a bunch of boilerplate, you can focus on writing your code π.
π INSTANCE ATTRIBUTES π
Okay bestie, so in Moose-land we declare an attribute with the has function, and that attribute holds your instance data. Cute, classic, we love her. That function is still here in MooseX::Extended, totally unchanged β but now there are two new icons on the scene: param and field, and they work a lot like has. Both of them default to is => 'ro', so if your attribute is read-only you can just... not say it. Skip the ceremony. π
A param is a required parameter (defaults are allowed tho). A field is not meant to be passed to the constructor (but hold on, there's a whole extended explanation below, don't @ me yet). Why does this matter? Because now any dev β writing or just reading the code β can tell at a glance what the class interface actually is. No more detective work. π
So instead of this whole situation, where the poor maintenance programmer is sitting there like "wait, what am I allowed to pass in here?? π"
has name => (...);
has uuid => (...);
has id => (...);
has backlog => (...);
has auth => (...);
has username => (...);
has password => (...);
has cache => (...);
has this => (...);
has that => (...);
You get this:
param name => (...);
param backlog => (...);
param auth => (...);
param username => (...);
param password => (...);
field cache => (...);
field this => (...);
field that => (...);
field uuid => (...);
field id => (...);
Interface? Crystal clear. Iconic. β¨
β¨ param β¨
param name => ( isa => NonEmptyStr );
Basically Moose's has, but make it mandatory. A param is required, periodt. You can pass it to the constructor, or you can hand it a default or a builder to supply the value instead.
That param up there? Lowkey exactly the same as writing all this out:
has name => (
is => 'ro',
isa => NonEmptyStr,
required => 1,
);
Now β if you want a parameter that has no default and no builder, and it's fine for it to optionally get passed to the constructor, just say required => 0.
param title => ( isa => Str, required => 0 );
Heads up though: param, just like field, defaults to read-only, is => 'ro'. But you can override that, no problem:
param name => ( is => 'rw', isa => NonEmptyStr );
# or
param name => ( is => 'rwp', isa => NonEmptyStr ); # adds _set_name
Other than that, it behaves like has. Any arguments that has accepts? You can pass them in. All of them. π«Ά
# we'll make it private, but allow it to be passed to the constructor
# as `name`
param _name => ( isa => NonEmptyStr, init_arg => 'name' );
The param's is option accepts rwp, just like Moo does. It'll create a writer named _set_${attribute_name|.
β¨ field β¨
field cache => (
isa => InstanceOf ['Hash::Ordered'],
default => sub { Hash::Ordered->new },
);
Again: like Moose's has. But a field is not intended to be passed to the constructor β you just use default or builder like normal. π
That field up there is equivalent to:
has cache => (
is => 'ro',
isa => InstanceOf['Hash::Ordered'],
init_arg => undef, # not allowed in the constructor
default => sub { Hash::Ordered->new },
lazy => 1,
);
And yes β field, same as param, defaults to read-only, is => 'ro'. Override away:
field some_data => ( is => 'rw', isa => NonEmptyStr );
#
field some_data => ( is => 'rwp', isa => NonEmptyStr ); # adds _set_some_data
Otherwise it behaves like has, and you can pass in any arguments has accepts. π
The field's is option accepts rwp, like Moo. It'll create a writer named _set_${attribute_name|.
Now, listen. If you pass field an init_arg with a defined value, the code will usually throw a Moose::Exception::InvalidAttributeDefinition exception. Usually. BUT β if the init_arg begins with an underscore, it's allowed. That's a deliberate loophole so devs writing tests can supply their own values more easily. iykyk. π§ͺ
field cache => (
isa => InstanceOf ['Hash::Ordered'],
default => sub { Hash::Ordered->new },
init_arg => '_cache',
);
With that in place, you can pass _cache => $my_testing_cache in the constructor. Slay.
A field is automatically lazy if it has a builder or a default. Why? Because there's no guarantee the code will ever call them β and this makes it so easy for a field to lean on a param value that's already sitting there. This is a classic Moose problem btw: attribute initialization happens in alphabetical order, so if you define an attribute whose default or builder depends on another attribute, you have to remember to name them right or declare the field as lazy. Annoying. π
Which does mean: if you actually need a field to be initialized at construction time, you have to be careful and declare that it's not lazy:
field created => ( isa => PositiveInt, lazy => 0, default => sub {time} );
In our opinion, this tiny little nit is a totally fair trade-off for this issue:
package Person {
use Moose;
has name => ( is => 'ro', required => 1 );
has title => ( is => 'ro', required => 0 );
has full_name => (
is => 'ro',
default => sub {
my $self = shift;
my $title = $self->title;
my $name = $self->name;
return defined $title ? "$title $name" : $name;
},
);
}
my $person = Person->new( title => 'Doctor', name => 'Who' );
say $person->title;
say $person->full_name;
Look at that code. It looks fine. It is not fine. π In the above, $person->full_name is always undefined, because attributes get processed in alphabetical order β so the full_name default code runs before name or title is ever set. Oops! To make it actually work you're required to add lazy => 1 to the full_name attribute definition.
Here's the same code for MooseX::Extended. It works correctly:
package Person {
use MooseX::Extended;
param 'name';
param 'title' => ( required => 0 );
field full_name => (
default => sub {
my $self = shift;
my $title = $self->title;
my $name = $self->name;
return defined $title ? "$title $name" : $name;
},
);
}
Note that param is not lazy by default β but you can add lazy => 1 if you need to.
NOTE: We were sorely tempted to change attribute field definition order from alphabetical to declaration order, as that would also solve the above issue (and might allow for deterministic destruction), but we decided to play it safe.
πΈ Attribute shortcuts πΈ
When you're using field or param (but not has), we've got some attribute shortcuts for you:
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 should be pretty self-explanatory, but if you want the full explanation, go see MooseX::Extended::Manual::Shortcuts. π
π« EXCLUDING FEATURES π«
Sometimes a feature is just... annoying. Or worse, it could cause actual bugs β like, if you have a croak method, our importing of Carp::croak is gonna be a whole problem. π
So for example, if you wish to eliminate MooseX::StrictConstructor along with the carp and croak behavior:
use MooseX::Extended excludes => [qw/StrictConstructor carp/];
Here's everything you're allowed to exclude:
StrictConstructor
use MooseX::Extended::Role excludes => ['StrictConstructor'];
Excluding this will no longer import MooseX::StrictConstructor.
autoclean
use MooseX::Extended::Role excludes => ['autoclean'];
Excluding this will no longer import namespace::autoclean.
c3
use MooseX::Extended::Role excludes => ['c3'];
Excluding this will no longer apply the C3 mro.
carp
use MooseX::Extended::Role excludes => ['carp'];
Excluding this will no longer import Carp::croak and Carp::carp.
immutable
use MooseX::Extended::Role excludes => ['immutable'];
Excluding this will no longer make your class immutable.
true
use MooseX::Extended::Role excludes => ['true'];
Excluding this will require your module to end in a true value.
param
use MooseX::Extended::Role excludes => ['param'];
Excluding this will make the param function unavailable.
field
use MooseX::Extended::Role excludes => ['field'];
Excluding this will make the field function unavailable.
π TYPES π
We bundle MooseX::Extended::Types so it's easy-peasy to have compile-time type checks, plus type checks inside functions. Here's a silly little example:
package Not::Corinna {
use MooseX::Extended types => [qw(compile Num NonEmptyStr ArrayRef)];
use List::Util ();
# these default to 'ro' (but you can override that) and are required
param _name => ( isa => NonEmptyStr, init_arg => 'name' );
param title => ( isa => NonEmptyStr, required => 0 );
# fields must never be passed to the constructor
# note that ->title and ->name are guaranteed to be set before
# this because fields are lazy by default
field name => (
isa => NonEmptyStr,
default => sub ($self) {
my $title = $self->title;
my $name = $self->_name;
return $title ? "$title $name" : $name;
},
);
sub add ( $self, $args ) {
state $check = compile( ArrayRef [ Num, 1 ] );
($args) = $check->($args);
return List;:Util::sum( $args->@* );
}
}
See MooseX::Extended::Types for more information. π
β¨ ASSEMBLING YOUR OWN MOOSE β¨
Okay bestie, so here's the tea β β once you've been living in MooseX::Extended for a while, you might start feeling a way about trading one pile of boilerplate for a different pile of boilerplate. And honestly? don't do that to yourself. Instead, build your own. Make it custom. Make it you. π
So first, define your very own version of MooseX::Extended:
package My::Moose::Role {
use MooseX::Extended::Role::Custom;
sub import {
my ( $class, %args ) = @_;
MooseX::Extended::Role::Custom->create(
excludes => [qw/ carp /],
includes => ['multi'],
%args # you need this to allow customization of your customization
);
}
}
# no need for a true value
π«Ά that %args line at the end is doing the lord's work β it's what lets you customize your customization. Don't skip it.
And then, like, actually go use it:
package Some::Class::Role {
use My::Moose::Role types => [qw/ArrayRef Num/];
param numbers => ( isa => ArrayRef[Num] );
multi sub foo ($self) { ... }
multi sub foo ($self, $bar) { ... }
}
Obsessed. See MooseX::Extended::Custom for more info, bestie. π
π ROLES π
Of course we support roles. Obviously. We're not monsters. π€ Here's a cute lil role that adds a created field to your class:
package Not::Corinna::Role::Created {
use MooseX::Extended::Role types => ['PositiveInt'];
# mark it as non-lazy to ensure it's run at construction time
field created => ( isa => PositiveInt, lazy => 0, default => sub {time} );
}
And then you consume it exactly like you'd consume any other role, no special treatment, no drama:
package My::Class {
use MooseX::Extended types => [qw(compile Num NonEmptyStr Str PositiveInt ArrayRef)];
with qw(Not::Corinna::Role::Created);
...
}
Peep MooseX::Extended::Role for the full rundown on what features it gives you and how to tweak its behavior. πβ¨
π¦ MIGRATING FROM MOOSE π¦
If your Moose hierarchy is clean and well-behaved (iconic behavior, truly), switching to MooseX::Extended is often literally just swapping Moose for MooseX::Extended and running your tests. That's it. Then you can start deleting all that boilerplate you've been dragging around β like, say, the make_immutable call. Freeing. ποΈ
But ngl, a LOT of Moose hierarchies are not clean. π You might fail on the StrictConstructor, or discover you're using multiple inheritance and relying on dfs (depth-first search) instead of the C3 mro, or β horrors! β you've got classes that aren't declared as immutable and you've got code that straight-up relies on that. If that's your situation, a brute-force approach to handling it could be the following:
package My::Moose {
use MooseX::Extended::Custom;
sub import {
my ( $class, %args ) = @_;
MooseX::Extended::Custom->create(
excludes => [qw/
StrictConstructor autoclean
c3 carp
immutable true
field param
/],
%args # you need this to pass your own import list
);
}
}
# no need for a true value
So with all of the above, you've excluded pretty much everything except signatures and postderef features (we will work on getting around that limitation, promise π«‘). From there, you can swap Moose out for My::Moose (and do something similar with roles) and it should just... work. β¨ Then the fun part: start slowly deleting items from excludes until your tests fail, and address them one-by-one. It's giving archaeology. It's giving patience. It's giving glow up. π
π MOOSE INTEROPERABILITY π
Moose and MooseX::Extended should be 100% interoperable. If it's not, please let us know, bestie, we want the receipts: <https://github.com/Ovid/moosex-extended/issues>.
π VERSION COMPATIBILITY π
We use GitHub Actions <https://github.com/features/actions> to run full continuous integration tests on versions of Perl from v.5.20.0 and up. And no cap: we do not release any code that fails any of those tests. Periodt. π
π AUTHOR π
Curtis "Ovid" Poe <curtis.poe@gmail.com>
π COPYRIGHT AND LICENSE π
This software is Copyright (c) 2022 by Curtis "Ovid" Poe.
This is free software, licensed under:
The Artistic License 2.0 (GPL Compatible)
And that's a wrap, bestie!! π This whole thing came to you via perl v5.42.0 on 2025-07-25, from the good people at User Contributed Perl Documentation for MooseX:...utorial(3). Go forth and make your own Moose, you absolute main character. π¦πβ¨
π¦ where this page comes from
- fedora 43: perl-MooseX-Extended 0.35-6.fc43 Β· Artistic-2.0
- π girlified by deepseek-ai/DeepSeek-V4.1-Flash on : a language model's rewrite of the original, under its terms