Copyright Notice
This text is copyright by InfoStrada Communications, Inc., and is used with their permission. Further distribution or use is not permitted.This text has appeared in an edited form in Linux Magazine magazine. However, the version you are reading here is as the author originally submitted the article for publication, not after their editors applied their creativity.
Please read all the information in the table of contents before using this article.
Linux Magazine Column 95 (Jul 2007)
[suggested title: ``The Moose is Flying (part 2)'']
Last month, I introduced the Moose
object system by walking through
the code developed in the perlboot manpage, rewriting it to use
Moose. Continuing from that discussion, let's look at some of the
features I didn't cover last month.
Our Animal
role included attributes of name
, and color
,
and actions of speak
and eat
. We can add a birthdate attribute
to the mix with:
has 'born' => (is => 'ro');
As we don't want to be able to change the birthdate, we'll make it read-only. By default, this attribute accepts any scalar, so these are all equally valid:
my $horse = Horse->new(born => 'yesterday', name => 'Newbie'); my $cow = Cow->new(born => 'spring of 82', name => 'Bessie'); my $mouse = Mouse->new(name => 'Minnie', born => '3/14/1929'); my $racehorse = RaceHorse->new(name => 'Slew', born => [3, 5, 59]);
We can get a bit of help from Moose to narrow down the permissible type of this new birthdate attribute using Moose's typing system:
require DateTime; has 'born' => (is => 'ro', isa => 'DateTime');
The isa
parameter here declares that the born
parameter must be
a DateTime object, or at least something that responds true to
UNIVERSAL::isa($thing, "DateTime")
.
Now, if we try to put anything in this born
attribute other than
a DateTime
, we get a run-time error. So, this fails:
my $horse = Horse->new(born => 'yesterday', name => 'Newbie');
But this succeeds:
my $horse = Horse->new(born => DateTime->now, name => 'Newbie');
The DateTime
string for isa
refers here to the Perl class.
However, we can also define this as an artificial Moose type:
use Moose::Util::TypeConstraints; require DateTime; subtype 'DateTime' => as 'Object' => where { $_->isa('DateTime') };
This works like before, but now identifies DateTime
as a
Moose-type. The type is created by starting with any Object
, and
then requiring that this object pass the additional qualification of
being a subclass of Perl's DateTime
.
At this point, we could continue to use this Moose-style DateTime type as we had before. But once we've done this, we can further subtype the type. For example, we can require that the date be a historical date (before now):
subtype 'HistoricalDateTime' => as 'DateTime' => where { $_ <= DateTime->now }; has 'born' => (is => 'ro', isa => 'HistoricalDateTime');
Now, not just any DateTime will do. It has to be something that isn't
in the future. The expression in where
can be any expression
returning a true/false value, using $_
as a proxy for the object in
question.
It'd be easier if we could still use casual forms like yesterday
and 3/14/1929
. These are both understood by Date::Manip
. We
could parse the strings with Date::Manip
, then pull out the component
values and hand them to DateTime
, but luckily, there's already
a module to do that: DateTime::Format::DateManip
.
use DateTime::Format::DateManip; my $yesterday = DateTime::Format::DateManip->parse_datetime('yesterday'); my $newbie = Horse->new(born => $yesterday, name => 'Newbie');
Not bad. Our newbie horse is born yesterday, as we expect. But
it'd be nice to just drop yesterday
into the slot and have it do
all of that for us. And with coercions, we can.
Since passing a simple string as the birthtime is illegal, we can
inform Moose that it should take that string and run it through
DateTime::Format::DateManip
automatically:
coerce 'HistoricalDateTime' => from 'Str' => via { require DateTime::Format::DateManip; DateTime::Format::DateManip->parse_datetime($_); };
The via
code block takes $_
as the input value, which is
expected to be a string (Str
). The last expression evaluated in
the code is the new HistoricalDateTime
value. We then permit this
coercion to be used by adding coerce
into the attribute
declaration:
has 'born' => (is => 'ro', isa => 'HistoricalDateTime', coerce => 1, );
Now, the born
slot accepts either an explicit DateTime
as
before, or a simple string. The string must be acceptable to
Date::Manip
, which will be used to convert the string into a
DateTime
object as our object is created.
my $newbie = Horse->new(born => 'yesterday', name => 'Newbie'); my $mouse = Mouse->new(name => 'Minnie', born => '3/14/1929');
Also, because the type constraint is still in place, the verification to ensure the result is a historical date is still active.
Besides the named classes and Str
, Moose::Util::TypeConstraints
also establishes types of things like Bool
and HashRef
.
We could even have multiple coercions defined, as long as they are
distinct. For example, we could use a hashref in the birthtime slot
to indicate that we are passing key/value pairs to be handed to a
DateTime
constructor directly:
coerce 'DateTime' => from 'HashRef' => via { DateTime->new(%$_) };
And now we can use a hashref to define the birthdate:
my $mouse = Mouse->new( name => 'Minnie', born => { month => 3, day => 14, year => 1929 }, );
If the value for born
is a DateTime
, it's used directly. If
it's a string, it's passed to DateTime::Format::DateManip
. And if
it's a hashref, it's passed directly as a flattened list to the
DateTime
constructor. Very cool.
The value we specify for a default
is subject to the same coercions
and type checking. We can update born
as:
has 'born' => (is => 'ro', isa => 'HistoricalDateTime', coerce => 1, default => 'yesterday', );
And now our animals default to being ``born yesterday''. Note that the
default value is still subject to the type constraints, so if we
replace yesterday
with tomorrow
, the default value will be
rejected properly. Note that the coercion happens as each object is
created, so a default value of one minute ago
will give us a new
time each time it is called.
While we're on things we can do to attributes, another interesting
item is lazy
. If the default value is expensive to compute, we can
say ``don't really do this until you need to do it''. For example,
turning yesterday
into a DateTime is slightly expensive, so we
can flag that as lazy:
has 'born' => (is => 'ro', isa => 'HistoricalDateTime', coerce => 1, default => 'yesterday', lazy => 1, );
Speaking of great support, as I was writing that last paragraph, I noticed a bug in the result, and chatting with Stevan on IRC got it fixed before I could turn this article in. Yeay.
Anything built with Moose
has a very high degree of introspection
available. For example, we can ask one of our animal friends to give us
the meta
object, with which we can make further requests:
my $horse = Horse->new; my $meta = $horse->meta; # or equivalently, Horse->meta
$meta
is a Moose::Meta::Class
. We can ask the horse for the
roles:
my @roles = @{$meta->roles};
In this case, we see that we've mixed in one role (of type Moose::Meta::Role
,
and get the name with:
map { $_->name } @roles; # qw(Animal)
which gives us Animal
as we might expect. We can also ask
the meta object for all applicable methods:
my @methods = $meta->get_method_list;
which returns
BEGIN born color default_color eat meta name private_set_color sound speak
Nice. Not sure what BEGIN is doing in there, but the rest are things
that we've defined. Many of these methods relate to the attributes,
but we'd can query those definitively using
compute_all_applicable_attributes
instead:
my @attrs = $meta->compute_all_applicable_attributes;
The result is a set of Moose::Meta::Attribute
objects. We can
map them through name
as before to get the names:
map { $_->name } @attrs; # qw(born color name)
We can also see if they have setters:
grep { $_->has_writer } @attrs; # qw(color)
Note that only color
has a setter: the other two are read-only,
so this makes sense.
As noted above, Moose is still being actively developed, but is
production ready as long as you stick with things that work.
You'll find the latest Moose in the CPAN, along with some other
core plugins, typically in the MooseX
namespace.
For example, MooseX::Getopt
allows you to define your @ARGV
processing
using type coercion, type constraints, and all that jazz. I haven't had time
to play with that yet, but it's on my to-do list, so perhaps I'll cover that
in a future column.
Similarly, MooseX::Object::Pluggable
makes it easy to write classes
that are pluggable, meaning that they can work well with plugins
that define additional methods and attributes. (Think of a generic
web server or IRC bot object that has individually selectable
additional behaviors.) Again, I'm just noticing these, and they look
like they are worthy of their own descriptions later.
Also note that Moose
is itself built on Class::MOP
, which is a
framework for making class frameworks. Perhaps other projects besides
Moose will be using Class::MOP as a starting point as well. For
example, the infrastructure of Class::Prototyped
(which I use in my
own CGI::Prototype
) might be built on Class::MOP
, giving it more
flexibility and robustness.
I hope you've enjoyed this two-part introduction to Moose
. Have
fun playing with a production-ready flexible object-building system. Until next time, enjoy!