A Practical Guide to Moose
Chris Prather
Infinity Interactive
What Moose Is Not
- An experiment or prototype
- A toy
- Just another accessor builder
- A source filter or some other kind of Perl black magic
- A implementation of Perl 6 in Perl 5
What Moose Is
- A complete modern object framework for Perl
- Syntactic Sugar for Class::MOP
- Written by Stevan Little
- Based on the work done by Stevan and others for Pugs and Perl 6
- Influenced by CLOS or Common Lisp Object System, Smalltalk, Java, BETA, OCaml, Ruby and more
- Stable and used in Production systems
- Stevan declared Moose as ready for prime time on April 3, 2007 at version 0.18
- We use Moose in large applications for household name clients
- Currently at version 0.38 released Yesterday
A Simple Example
package Person;
use strict;
use warnings;
sub new {
my ($class) = @_;
return bless {
name => '',
age => undef,
}, $class;
}
sub name {
my ($self, $name) = @_;
$self->{'name'} = $name if $name;
return $self->{'name'};
}
sub age {
my ($self, $age) = @_;
$self->{'age'} = $age if $age;
return $self->{'age'};
}
1;
A Simple Moose Example
package Person;
use Moose;
has name => (is => 'rw');
has age => (is => 'rw');
1;
use Moose;
- imports
has, extends, with, before, after, around, super, override, inner, augment
- turns on
use strict; and use warnings;
Carp::confess and Scalar::Util::blessed
- sets
@ISA to Moose::Object (unless it’s already set)
A Simple Moose Example (cont.)
package Person;
use Moose;
has name => (is => 'rw');
has age => (is => 'rw');
1;
has declares attributes
- Moose will automatically create the proper accessors
is => 'rw' create read/write accessor
is => 'ro' create read only accessor
- Also note no constructor
Moose::Object provides one for us
Variations on a Moose Example
package Person;
use Moose;
has name => (
is => 'rw',
isa => 'Str'
default => 'Bob'
);
has staff => (
is => 'ro',
isa => 'ArrayRef',
lazy => 1,
default => sub { [qw(Bob Alice Tim)] },
);
1;
default values are either …
- non-references (numbers, strings)
- subroutine references
default sub is passed $self
lazy says not to run the default during object creation
- default will be run the first time
staff is called $self->staff
Variations on a Moose Example (cont.)
has name => (
is => 'rw',
isa => 'Str'
default => 'Bob'
);
isa sets up Type checking
- Types are defined in
Moose::Util::TypeConstraints.
- Standard type constraints:
Any, Item, Bool, Undef, Defined, Value, Num, Int, Str, Ref, ScalarRef, ArrayRef, HashRef, CodeRef, RegexpRef, GlobRef, FileHandle, Object and Role
- Type constraints are subtype-able
- Unknown types are assumed to be a Class and an anonymous subtype will be created
has 'date' => (isa => 'DateTime'); # This will DWIM
Some Type of Digression
has employees => (
is => 'rw',
isa => 'ArrayRef[Employee]',
);
has shopping_carts => (
is => 'rw',
isa => 'ArrayRef[ArrayRef[Items]]'
);
has language => (
is => 'rw',
isa => 'English | Welsh | Scots | Gaelic',
);
has member => (
is => 'rw',
isa => 'Employee | ArrayRef[Employee|Group]',
);
Some Type of Digression (cont.)
- You can also use CPAN modules that check data types
package Foo;
use Moose;
use Moose::Util::TypeConstraints;
use Declare::Constraints::Simple -All;
# define your own type ...
type 'HashOfArrayOfObjects'
=> IsHashRef(
-keys => HasLength,
-values => IsArrayRef( IsObject ));
has 'bar' => (
is => 'rw',
isa => 'HashOfArrayOfObjects',
);
- Use
Declare::Constraints from CPAN
- This example is taken straight from the Moose test suite
t/200_examples/004_example_w_DCS.t
Some Type of Digression (cont.)
package Foo;
use Moose;
use Moose::Util::TypeConstraints;
use Test::Deep qw(eq_deeply array_each subhashof ignore);
# define your own type ...
type 'ArrayOfHashOfBarsAndRandomNumbers'
=> where {
eq_deeply($_,
array_each(
subhashof({
bar => Test::Deep::isa('Bar'),
random_number => ignore()
})
)
)
};
has 'bar' => (
is => 'rw',
isa => 'ArrayOfHashOfBarsAndRandomNumbers',
);
Some Type of Coercion
- Moose can also convert from one type to another … with a little help.
package Employee;
use Moose;
use Moose::Util::TypeConstraints;
extends qw(Person);
subtype 'Manager'
=> as 'Object'
=> where { $_[0]->isa('Manager') };
coerce 'Manager'
=> from 'Str'
=> via { Manager->new( name => $_) };
has manager => (
is => 'ro',
isa => 'Manager',
required => 1,
coerce => 1,
);
Some Type of Coercion (cont.)
use Moose::Util::TypeConstraints;
- Make sure you use the TypeConstraints utilities
subtype 'Manager'
=> as 'Object'
=> where { $_[0]->isa('Manager') };
- You have to define your subtype(s)
- Types are global so you only need to do this once
coerce 'Manager'
=> from 'Str'
=> via { Manager->new( name => $_) };
- Then you tell Moose how to convert from one type to another
has manager => (
...
coerce => 1,
);
- Finally you tell Moose which attributes to coerce
Some Type of Coercion (cont.)
use Moose::Util::TypeConstraints;
subtype 'ArrayRef[Employee]' => as 'ArrayRef';
coerce 'ArrayRef[Employee]'
=> from 'ArrayRef[Str]'
=> via { [ map { Employee->new( name => $_ ) } @$_ ] };
has staff => (
is => 'ro',
isa => 'ArrayRef[Employee]',
lazy => 1,
default => sub { [qw(Bob Alice Tim)] },
coerce => 1,
);
- Type Coercions can also apply to defaults, and to Arrays
Conventional Delegates
package Employee;
use Moose;
extends qw(Person);
has manager => (
is => 'ro',
isa => 'Manager',
handles => {
manager_name => 'name',
coworkers => 'staff',
}
);
handles sets up a mapping between the current class, and the manager
$self->manager_extension will proxy $self->manager->phone_extension
Conventional Delegates (cont.)
- Handles can also take an arrayref if you don’t need to re-write the method names
has phone => (
...
handles => [qw(number extension)],
);
- Or a REGEXP or ROLE or CODE
- But I haven’t seen those used (yet)
- and the coderef option is considered “funky”
UnConventional Delegates
package Company;
use Moose;
use MooseX::AttributeHelpers;
has employees => (
metaclass => 'Collection::Array',
isa => 'ArrayRef[Employees]',
is => 'rw',
provides => {
push => 'add_employee',
pop => 'remove_employee',
count => 'number_of_employees',
empty => 'any_employees',
}
)
- Use
MooseX::AttributeHelpers from the CPAN
- It provides an attribute metaclass
- provides basic methods for standard data types
- supplies
provides parameter for attributes
UnConvetional Delegates (cont.)
use Moose::Autobox;
[ 1 .. 5 ]->map(sub { $_ * $_ }) # [ 1, 4, 9, 16, 25 ]
'Hello World'->index('World') # 6
- Using
Moose::Autobox from CPAN
- Allows you to call methods on references
- Comes with delegates for all the major players
Extending the Example
package Person;
use Moose;
has name => (is => 'rw');
has age => (is => 'rw');
package Employee;
use Moose;
extends qw(Person);
has manager => (
is => 'ro',
isa => 'Manager',
required => 1, # Everybody has to have a manager
);
package Manager;
use Moose;
extends qw(Employee);
has staff => (
is => 'rw',
isa => 'ArrayRef',
default => sub { [] },
);
Extending the Example (cont.)
package Employee;
use Moose;
extends qw(Person);
has manager => (
is => 'ro',
isa => 'Manager',
required => 1, # Everybody has to have a manager
);
extends declares the parent classes
extends is preferred over use base because it fully replaces the @ISA rather than extending it
- it defines the inheritance of the class, not an inheritance of the class
- this gurantees no hidden surprises, like classes not properly inheriting from
Moose::Object
- it also happens to work better with Perl 5.10
Aspects of Method Modification
- Moose provides the “Method Modifiers”
before, after and around
before 'employees' => sub { warn 'calling employees' };
- Executes just before the current method
- It gets a copy of @_
- The return value is ignored
after 'employees' => sub { warn 'finished calling employees' };
- Executes just after the current method
- It gets a copy of @_
- The retun value is ignored
Aspects of Method Modification (cont.)
- Around is a bit more complex
around 'employees' => sub {
my ($next, $self, @args) = @_;
...
my @return = $next->(@args);
....
return @return;
};
- Allows you to “monkey patch” methods automatically provided by Moose (or via a Role)
- You control if the original method is called at all ($next)
- The return value is not ignored.
Role of the Moose
package TeamMember;
use Moose;
extends qw(Employee);
with qw(ProjectTeamRole);
has 'weekly_hours' => (
is => 'ro',
isa => 'Int',
default => 40,
);
1;
with adds a Role (ProjectTeamRole) to the Class (TeamMember)
- A Role is a part of a Class that is composed into the class during Class creation … basically a Mix-In
- Similar in concept to Java’s Interfaces
- In fact Interfaces are a simplified case of Roles
- Roles are much more powerful
Moose::Object provides a does() method to check objects for Roles
$emp->does('ProjectTeamRole'); # true for TeamMembers
Role of the Moose (cont.)
package ProjectTeamRole;
use Moose::Role;
requires qw('weekly_hours');
has project => (
is => 'ro',
isa => 'Str',
required => 1,
);
has project_hours => (
is => 'rw',
isa => 'Int',
default => 0,
);
sub log_hours {
my ( $self, $hours ) = @_;
my $total = $self->project_hours + $hours;
$self->project_hours($total);
}
Role of the Moose (cont.)
package ProjectTeamRole;
use Moose::Role;
requires qw('weekly_hours');
use Moose::Role
- imports everything from Moose plus
requires and excludes
requires says that the composing class must have a weekly_hours method
- Note that Roles are only part of a Class
- They cannot inherit from parent classes …
extends throws an exception
- All other Moose keywords are deferred until the Class creation
Role of the Moose (cont.)
- Declare any additional attributes
has project => (
...
);
has project_hours => (
...
);
sub log_hours { ... };
- This is where they differ from Java Interfaces
- Interfaces only provide a “contract”, Roles provide an implementation
Role of the Moose (cont.)
- Role Composition is unordered and can get quite complex
package MyApp::Daemon;
use Moose::Role;
with 'MooseX::LogDispatch'; # provides log() method
with 'MooseX::Daemonize';
package MyApp2;
use Moose;
with 'MyApp::Daemon'; # provides log() method
with 'MyApp2::Logger'; # also provides log() method
- By default the
log would get dropped
- This is by design, there are maths that explain it
- I don’t fully understand them
- All is not lost though …
Role of the Moose (cont.)
- You can specify that a Role is incompatible with another Role using
excludes
package Molecule::Organic;
use Moose::Role;
excludes 'Molecule::Inorganic';
package Molecule::Inorganic;
use Moose::Role;
package My::ScienceProject;
use Moose;
with qw(Molecule::Organic Molecule::Inorganic); # dies
- We could set our Logger roles to exclude each other … but it doesn’t scale
Role of the Moose (cont.)
- Starting with Moose 0.34
with can also take parameters for composition
package MyApp2;
use Moose;
with 'MyApp::Daemon' => {
excludes => 'log',
alias => {
daemonize => 'run'
}
};
with 'MyApp2::Logger';
- So we can
exclude the log method from MyApp::Daemon
- We’ll also
alias daemonize (from MooseX::Daemonize) to be our run method
Role of the Moose (cont.)
- There are several pre-baked Roles on the CPAN that provide various features
MooseX::Storage - Serialization to JSON or YAML
MooseX::Daemonize - Daemonization and handling of pid files
MooseX::Getopt- Command Line Parsing
MooseX::Param - Provide a param() method similar to CGI.pm’s
MooseX::Workers - forked process management for concurrently running tasks
MooseX::LogDispatch - Provide a standard Logging object
Return of the Autobox
package Units::Bytes;
use Moose::Role;
use Moose::Autobox;
sub bytes { $_[0] }
sub kilobytes { $_[0] * 1024 }
sub megabytes { $_[0] * 1024->kilobytes }
sub gigabytes { $_[0] * 1024->megabytes }
sub terabytes { $_[0] * 1024->gigabytes }
Moose::Autobox->mixin_additional_role(SCALAR => 'Units::Bytes');
- You can define a custom Role to add more methods to Autoboxed variables
Return of the Autobox (cont.)
use Units::Bytes;
use Moose::Autobox;
is(5->bytes, 5, '... got 5 bytes');
is(5->kilobytes, 5120, '... got 5 kilobytes');
is(2->megabytes, 2097152, '... got 2 megabytes');
is(1->gigabytes, 1073741824, '... got 1 gigabyte');
is(2->terabytes, 2199023255552, '... got 2 terabyte');
- Then later you can use the custom autobox features
- taken from
t/003_p6_example.t in the Moose::Autobox dist
Metaclasses, Metaobjects, and the MOP (Oh My!)
- Moose is based on Class::MOP
- Full Metaobject Protocol for Perl5
- Fancy words for makes an object for everything
my $class = $obj->meta; # $obj's metaclass
my $meta = MyApp->meta; # MyApp's metaclass
my $really_meta = $obj->meta->meta # $obj's metaclass's metaclass
print $obj->meta->name; #print $obj's class' name
Looking in From the Inside
my $class = $self->meta;
- The
meta method returns the metaclass for every class and object
- Metaclass provides introspection
has_method determine if the class has a given method
compute_all_applicable_methods returns a list of all the methods for a class (including inherited)
has_attribute same for an attribute
compute_all_applicable_attributes same here too
Looking in From the Inside (cont.)
- You can even create an entire class programmatically.
Class::MOP::Class->create('Bar' => (
version => '0.01',
superclasses => [ 'Foo' ],
attributes => [
Class::MOP:::Attribute->new('$bar'),
Class::MOP:::Attribute->new('$baz'),
],
methods => {
calculate_bar => sub { ... },
construct_baz => sub { ... }
}
));
The Metaclass Tango
- Metaclassses allow you to override specific parts of how Moose does things
- You’ve already seen them at work
has employees => (
metaclass => 'Collection::Array',
...
)
MooseX::AttributeHelpers uses custom Attribute Metaclasses to do it’s job
- Attributes Metaclasses define how Attributes work
- You can override nearly any part of Moose by providing the right metaclass
Benefits of Moose
- Code is less tedious
- no need to worry about the basic mechanics of OO like:
- object initialization order
- object destruction order
- attribute storage, access and initialization
- less tedium means many (sloppy) typo errors are all but eliminated
- Code is shorter
- Moose’s declarative style allows you say more with less
- less code == less bugs
- Less low-level testing needed
- no need to verify things which are already checked by the Moose test suite
- tests can focus on what your class does, not that it is “assembled” correctly
- Code becomes more descriptive
- Moose’s declarative style requires you to provide more descriptive information
- Code describes your intentions more and OO mechanics less
- Unified Introspection
- Moose gives you hooks to every part of the Perl OO Bestiary
- And the Metaobject Protocol
Drawbacks of Moose
- Moose has a fairly heavy compile-time cost.
- Not appropriate for non-persistent environments (vanilla-CGI, etc.)
- MooseX::Compile on the CPAN is a start on reducing this burden.
- It pre-compiles everything to .pmc files
- Some Moose features can be slow at times.
- However speed is directly proportional to the amount of features used.
- Nothing in life is free.
- Extending non-Hash based classes is tricky.
- The most common example is the IO::* family of class.
- Stevan recommends Class::InsideOut or Object::InsideOut for this (or delegation, however …).
- That said, there is now MooseX::InsideOut for InsideOut objects
- and MooseX::GlobRef::Object for the GlobRefs like IO::*
- and MooseX::Singleton to enforce a Singleton pattern
- Some delegation features do not play well with AUTOLOAD
- AUTOLOAD is evil anyway :)
Le Fin
- Special thanks to
- Stevan and Robert Boone who’s previous talks formed the foundation of this one
- Joshua Jore (jjore), Christopher Humphries (chumphries) for the read through last night
- Matt Trout (mst), Yuval Kogman (nothingmuch), Jon Rockway (jrockway), Robert Sedlacek (phaylon), Ash Berlin (ash) and the rest of #moose
And you for coming
-Moose
- Moose One Liners with
oose.pm
perl -Moose -e'has foo => (is=>q[rw]); Class->new(foo=>1)'
- Very useful for testing if something works
- Nice for communicating ideas on IRC
- My first (and only) source filter
MooseX::POE
package Counter;
use MooseX::POE;
has count => (
isa => 'Int',
is => 'rw',
);
sub START {
my ($self) = @_;
$self->yield('increment');
}
event increment => sub {
my ($self) = @_;
print "Count is now " . $self->count . "
";
$self->count( $self->count + 1 );
$self->yield('increment') unless $self->count > 3;
};
Counter->new( count => 0 );
POE::Kernel->run()
- Every class becomes a POE::Session
events are methods that can run asynchronously