A detailed look at MooseX-Types

While most people in Mooseland are using its core typesystem as if it’s been there all the time. The MooseX-Types extension on the other hand is a bit too much magic without obvious advantages to many people. To some it might seem a lot of work to create yet another module just to put some types in. Others might not see any value in using exports instead of strings. With this article I’ll try to show the why’s, the pro’s and the yay’s of designing type libraries with MooseX-Types.

Namespacing

At first, MooseX-Types was built because Moose types are managed in a global registry. This is great, since this allows us to reference types by name somewhere without having to have them loaded. On the other hand though, having to fully qualify namespaces for types all the time was a bit annoying, especially in projects that make heavy use of types.

Type libraries were the implemented solution to this dilemma. You simply declare your types inside a library, and you can import them as symbols wherever you need them. Since we all like code, here’s a quick example for those who don’t know this extension yet.

First, the type library:

package MyProject::Types;
use MooseX::Types -declare => [qw( Pair )];

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

subtype Pair,
     as ArrayRef,
  where { @$_ == 2 };

1;

The above declares a type library in the package MyProject::Types. It also declares that it will define and provide the type Pair, which is later defined with as a subtype of ArrayRef. The latter is simply a type export from another library, called MooseX::Types::Moose, which is shipped with the MooseX-Types distribution. It provides type exports for all core types provided by Moose.

If we wanted to use this in our Moose class (I’ll use MooseX-Declare for the shine), it would look something like this:

use MooseX::Declare;

class MyProject::Point {

    use MyProject::Types qw( Pair );

    has coordinates => (
        is       => 'rw',
        isa      => Pair,
        required => 1,
    );
}

Here we import the Pair type into the class. Since we now can consistently refer to this type by using the export, we can ignore that its actual name is MyProject::Types::Pair. The amount of typing saved stands out even more if we add an additional type to our library:

subtype Coordinate,
     as Int,
  where { abs() <= 15 };

Of course we now also want to extend our point class to have proper coordinates:

has coordinates => (
    is       => 'rw',
    isa      => Pair[Coordinate],
    required => 1,
);

The actual name of the type constraint, if it were specified as a string, is MyProject::Types::Pair[MyProject::Types::Coordinate]. Nothing you’d want to type all the time. With the type library approach, we only have to write the library prefix of the types once per package.

Besides the practical advantage of shorter names without conflicting with other people’s types, many types simply aren’t specific to a package, but rather to the whole project. Even further, some types are so common, they should be separate distributions on CPAN (of which there already are some very useful ones). A type library allows you to bundle your types in a reusable manner.

The other Type of Type Safety

A nice side effect of using symbols instead of strings is that in combination with strict, which Moose turns on automatically, typos will be caught at compile time. To demonstrate the difference:

class Foo {
    use MooseX::Types::Moose qw( ArrayRef );

    has bar => (isa => Arrayref);
}

The code above will not compile, but die with an error about the Arrayref bareword that Perl does not know. If we use the plain Moose variant:

class Foo {

    has bar => (isa => 'Arrayref');
}

This code won’t die at compile time, it won’t die at all. If we would run the statement

Foo->meta->get_attribute('bar')->type_constraint->parent;

we’d see that the type constraint became a subtype of Object. This isn’t actually bad. Moose falls back to a class type autovivification that is basically a shortcut to creating a subtype for every class.

Currently, I even prefer to mix the use of strings and type library exports depending on what type I mean. Although actually I’m not really using strings, I use aliased to load and shorten other class names I use in my code. A full example would look something like this:

class MyProject::Line {

    use MyProject::Types qw( Pair );
    use aliased 'MyProject::Point';

    has coordinates => (
        is       => 'rw',
        isa      => Pair[Point],
        required => 1,
    );
}

In the above example, Pair is the type export we already know, while Point is basically a constant string containing the package name MyProject::Point. This way, any typos will be caught at compile time, but I don’t have to declare a type for every isa check I want to do on an Object attribute.

First Class Objects

Moose types are simple objects under the hood, which means they can be passed around like any other value. While this might not look that sharp at first, we can do quite some nice things with this.

Here for example is a customized version of grep working with type constraints:

sub typegrep {
    my ($type, @list) = @_;
    return grep { $type->check($_) } @list;
}

# this will output 1 and 17
say for typegrep Int, 1, 'a', [], 3.5, 17;

Now, this is rather easy stuff. But checking values is not all of the functionality that Moose types provide. We can also use the coercions in a first class style:

sub coercer {
    my ($type, $alt) = @_;

    return sub { 
        map {
            my $value = $type->coerce($_);
            $type->check($value) ? $value : $alt;
        } @_ 
    };
}

subtype MightyInt, as Int;

coerce MightyInt,
    from Str, via { ord },
    from ArrayRef, via { scalar @$_ };

my $coercer = coercer MightyInt, -1;

# this will output 1, -1, 3, 97 and 7
say for $coercer->(1, 3.5, [1, 2, 3], 'a', 7);

The simple coercer routine above can now be used to build callbacks that will try to coerce a list of values to the target type, or return an alternative value for those that can’t be coerced (the alternative defaults to the undefined value).

The list of possibilities goes on and on. On the top of my head, here are some more rather unusual things that became very easy with Moose type constraints:

  • Introspectable validation and predicates: Reaction uses the type constraints on attributes to render interfaces.

  • The ability to abstract away the sources of data. You can use type constraints to find all values of a specific type in a list, like above, or a tree of objects, to give a more complex example. But you could simply ask the tree for all his nodes of a specific type. The difference is that how the tree searches its nodes, or even how it finds them, is up to the tree implementation.

More Resources

Since it was started, MooseX-Types got a lot of support from the community, It is now much more flexible than in the early days, and many people have released their own type libraries on CPAN:

I hope this clears up some fears, misunderstandings, or even just informs some people that this module exists. As always, feedback is very welcome.

No TrackBacks

TrackBack URL: http://www.catalyzed.org/mt/mt-tb.fcgi/45

5 Comments

| Leave a comment

This is all very elegant indeed. However, I would like to invite folks to discuss the pros and cons of this approach, for example in light of any extra overhead incurred through its implementation. I'm not worried that it might cause sever bottlenecks, but it is still nice to weigh the benefits with the disadvantages (if any).

The only overhead would be at startup time, and it's overshadowed by Moose's other startup costs anyway. This is negligible.

The problem MooseX::Types was solving was a cultural one, people were polluting public namespaces with global types, and like all global namespacing scheme this goes very wrong very fast (c.f. PHP).

The technical issues with it mostly involve people getting confused by previous idioms (it can't magically make a string resolve as a using the properly scoped namespace), and that class names (which are actually string barewords) conflict with the namespace for subroutine barewords, so you have to do things like DateTime::->new (because DateTime->new would call that method on the type constraint object).

I recently stumbled across this site and I have to say, another very useful Moose related example.

Keep 'em coming!

I don't know if I'm the only one who would like to see this, but I think a good example of how to properly use DBIx::Class and Moose together would be excellent. The Moose documentation is lacking in this area...

(Sorry, my english is bad)

> The technical issues with it mostly involve people getting
> confused by previous idioms (it can't magically make a string
> resolve as a using the properly scoped namespace), and that class
> names (which are actually string barewords) conflict with the
> namespace for subroutine barewords, so you have to do things like
> DateTime::->new (because DateTime->new would call that method on
> the type constraint object).

This is something i didn't think about.

To "resolve" this, i think if it is possible to add a function that can be importet that return the Types.

Instead of
> use My::Types qw(DateTime);
> ...
> is => DateTime
> ...

something like
> use My::Types qw(type);
> ...
> is => type("DateTime")
> ...

and the type() function return the "My::Type::DateTime" type. And a normal

"DateTime->new" just creates a new DateTime object like intended.

I think this is even short enough. And because this way is optional you can even use the old way if you never do "DateTime->new" by yourself.

Would that be a good addition for MooseX::Types?

@Sid Burn,

That would work, and I could even imagine someone wanting and writing such an extension. Personally though, I wouldn't use it since you'd lose the compile-time error checking.

I myself simply do

use My::Types DateTime => { -as => 'DateTimeType' }

Leave a comment

All comments are moderated. Spammers don't waste your time

Sponsored By


Ionzero: Rescue your dev project.

Following

Not following anyone

Note to spammers: all comments are moderated. Don't waste your time