Title: A Practical Guide to Moose Location: Frozen Perl Workshop Presenter: Chris Prather Organization: Infinity Interactive Date: 2008-01-17 Theme: moose Location: Frozen Perl Workshop 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
=======================
* Parameterized Types
has employees => (
is => 'rw',
isa => 'ArrayRef[Employee]',
);
has shopping_carts => (
is => 'rw',
isa => 'ArrayRef[ArrayRef[Items]]'
);
* Union of Types
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 = $self->$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 => (
...
);
* And methods
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
===================
* Metaclasses 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 :)
✂------✂------✂------✂------✂------✂------✂------✂------✂------✂------
La Fin
======
* Special thanks to
* Stevan and Robert Boone who's previous talks formed the foundation of this one
* Matt Trout (mst), Yuval Kogman (nothingmuch), Jon Rockway (jrockway), Robert Sedlacek (phaylon), Ash Berlin (ash) and the rest of #moose
* Robin Berjon(darobin), Joshua Jore (jjore), Christopher Humphries (chumphries) and others for the proofreading
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 . "\n";
$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