Telephone +44(0)1524 64544
Email: info@shadowcat.co.uk

MooseX::Types - because typing is hard

Fri May 18 15:00:00 2012

MooseX::Types - because typing is hard

Moose is awesome; we all know that. However since types are just as global as package names, it's very easy to run into trouble with namespacing - either you use short names, and risk clashes with other packages, or you use fully namespaced names, and incur a lot of extra typing. Or ... there's a third way, but first, a little background.

Going under an alias

For class names, the excellent aliased allows you to replace

use MyCompany::MyProject::SomethingOrOther::Frobnicator;

...

my $frobber = MyCompany::MyProject::SomethingOrOther::Frobnicator->new(...);

with

use aliased 'MyCompany::MyProject::SomethingOrOther::Frobnicator';

...

my $frobber = Frobnicator->new(...);

which is stonkingly less annoying. Since Moose takes a class name as a type name to restrict you to objects of that class, you can even replace

has frobber => (
  is => 'ro',
  isa => 'MyCompany::MyProject::SomethingOrOther::Frobnicator'
);

with the substantially less irritating

has frobber => (is => 'ro', isa => Frobnicator);

Making a hash of things

So far so good. But as soon as we try to make a typed map of a class, it starts to go horribly wrong.

has frobbers => (
  is => 'ro',
  isa => 'HashRef[MyCompany::MyProject::SomethingOrOther::Frobnicator]'
);

can't take advantage of the aliased magic because the type name is a string. Well, we could do

has frobbers => (is => 'ro', isa => 'HashRef['.Frobnicator.']');

or

has frobbers => (is => 'ro', isa => "HashRef[${\Frobnicator}]");

but then the next person to try and maintain this code is significantly likely to hunt you down and punch you repeatedly in the genitals, and rightly so.

Enter the MooseX

I said MooseX. Leave the cow alone.

MooseX::Types is a system for building namespaced, re-usable type libraries that then export subroutines returning the constraint, much like aliased does for class names (although MooseX::Types returns an object rather than a string).

Better still, there's a bunch of already pre-existing ones in that namespace, including a Moose one that provides subroutine forms of the built-in type constraints - including parameterizable versions of types like HashRef that provide the same syntax as the string form.

So, the first thing we can do is to use MooseX::Types::Moose to get a subroutine style HashRef that can handle this a bit better -

use aliased 'MyCompany::MyProject::SomethingOrOther::Frobnicator';
use MooseX::Types::Moose qw(HashRef);

has frobbers => (is => 'ro', isa => HashRef[Frobnicator]);

Alarums and coercions

Next problem - coercions. The thing about Moose coercions is that they're attached to the type (a fact that sometimes makes me sad, but is usually perfectly workable), so ideally you need to add them to your own type.

None of the core Moose types have coercions. None of your core-like ones should either.

In practice, this means that

coerce 'MyCompany::MyProject::SomethingOrOther::Frobnicator',
  from HashRef,
  via { MyCompany::MyProject::SomethingOrOther::Frobnicator->new(%$_) };

is almost certainly the wrong thing to do.

For added comedy value, it will throw an exception ... sometimes. This is because types should be defined before they're used - strictly, this code should be preceded by

class_type 'MyCompany::MyProject::SomethingOrOther::Frobnicator';

but since 'use Moose' in your class does that anyway, if the class has already been loaded whne you call coerce, it all appears to be fine. Then your loading order changes and BOOM. I love bugs like this; they make "No boom today? Boom tomorrow. There's always boom tomorrow." seem delightfully predictable.

So, a common way to handle this is:

class_type 'MyCompany::MyProject::SomethingOrOther::Frobnicator';

subtype 'MyCompany::MyProject::Types::Frobnicator',
  as 'MyCompany::MyProject::SomethingOrOther::Frobnicator';

coerce 'MyCompany::MyProject::Types::Frobnicator',
  from HashRef,
  via { MyCompany::MyProject::SomethingOrOther::Frobnicator->new(%$_) };

Are you ready to commit hara-kiri yet?

How many types would a MooseX::Types type?

Happily, there is an answer to this - MooseX::Types not only provides lots of pre-built type libraries but the tools to build your own. For example,

package MyCompany::MyProject::Types;

use aliased
  'MyCompany::MyProject::SomethingOrOther::Frobnicator'
     => 'Frobnicator_Class';

use MooseX::Types::Moose qw(ArrayRef HashRef);

use MooseX::Types -declare => [ qw(Frobnicator) ];

subtype Frobnicator, as Frobnicator_Class;

coerce Frobnicator, from HashRef, via { FrobnicatorClass->new(%$_) };

will allow you, in other code, to write

package ...;

use Moose;
use MyCompany::MyProject::Types qw(Frobnicator);

has frobber => (is => 'ro', isa => Frobnicator, coerce => 1);

to automatically invoke the coercion without affecting any code in another project that's also for some reason using your fine frobnication facilities.

An array(ref) of possibilities

Finally, we need to deal with the ArrayRef[] case, so we adjust our declaration to

use MooseX::Types -declare => [ qw(Frobnicator Frobnicator_ArrayRef) ];

and add below the first definition:

subtype Frobnicator_ArrayRef, as ArrayRef[Frobnicator];

coerce Frobnicator_ArrayRef,
  from ArrayRef[HashRef],
   via { [ map Frobnicator_Class->new({ %$_ }), @{$_} ] };

at which point

has many_frobbers => (is => 'ro', isa => Frobnicator_ArrayRef, coerce => 1);

will happily take

[ { ... }, { ... }, ... ]

and turn it into an arrayrefs of objects for you.

But wait! There's more! What we almost certainly really want is to handle an ArrayRef of objects or hash references, so instead we want to coerce from an arrayref of a union type - which we can make using the | operator:

coerce Frobnicator_ArrayRef,
  from ArrayRef[HashRef|Frobnicator],
   via {
     [
       map +(
         blessed($_)
           ? $_
           : Frobnicator_Class->new({ %$_ })
        ), @{$_}
      ]
    };

... aaand that's far too much typing again, and won't change nicely if we alter the frobnicator coercion. So, to simplify, we use the to_Type sub that MooseX::Types creates for us:

coerce Frobnicator_ArrayRef,
  from ArrayRef,
   via { [ map to_Frobnicator($_), @{$_} ] };

and on failure the to_ subroutine will return undef, which Moose will happily trap for us (a defined+die check might produce a nicer error; you're welcome to add that if it annoys you).

Showing some class

There has, historically, been one annoyance of MooseX::Types - that the exported type constants return only a type constraint, so for example

use MyCompany::MyProject::Types qw(Frobnicator);

my $frobber = Frobnicator->new;

used actually construct a new type constraint object rather than a frobnicator, which is at best surprising, and at worst downright enraging.

Fortunately, this is now fixed - if your type is a class type, or a subtype of a class type, MooseX::Types::TypeDecorator goes and finds the class and checks ->can on it first - and if there's a method of that name, uses that (and if there isn't, falls back to the type constraint object so generally stuff that treats it as one of those doesn't notice the difference).

That means that (as of version 0.32 of MooseX::Types)

has frobber => (
  is => 'ro', isa => Frobnicator, coerce => 1,
  default => sub { Frobnicator->new(frob_vigorously => 1) }
);

successfully manages to do what you expect.

Typing is hard - let's go moosing

So, given all of the above, so far as I'm concerned there's now absolutely no excuse not to use type libraries and namespaced types - it's less typing, less hassle, avoids clashes, and better still if you mistype a type name then perl gets to scream at your right then and there, which it sadly can't do when you typo something in a string.

So, in summary: Moose rocks. So does MooseX::Types. Go forth and use them, and be happy.

-- mst, out