Keeping your Coercions to yourself

Last time we took a detailed look at MooseX-Types. In today’s article I want to show you the implications of coercions to global types, why you shouldn’t do it, and how to do it better.

The Problem

The first and foremost principle is that since Moose types are stored in a global registry, you should properly namespace your subtypes (done for you by building a MooseX-Types library) to avoid collisions, and you should never coerce to a global type, only from it (global types include the builtin types, class types, role types, and whatever might come up in the future along those).

The reason for this is that like with type names, coercions can collide. Imagine you have written this type library below:

package MyLibrary;
use MooseX::Types -declare => [qw( Foo )];
use MooseX::Types::Moose qw( ArrayRef );

subtype Foo, as Int, where { $_ > 0 };

coerce ArrayRef, from Foo, via { [$_] };

1;

Can you already sense the problem? Let’s now assume you find this wonderful library named Bar on CPAN, and want to use it. Since the author of that module is a fan of Moose, just like we are, he also used types. This shiny distribution could contain this type library:

package OtherLibrary;
use MooseX::Types -declare => [qw( Bar )];
use MooseX::Types::Moose qw( ArrayRef );

subtype Bar, as Int, where { $_ > 0 and $_ % 2 };

coerce ArrayRef, from Bar, via { [1 .. $_] };

1;

There you have it. A simple, elegant and collaborative implementation of a decent headache. In case you’re not that familiar with Moose types or coercions, look at this typical usage example of types:

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

    has items => (is => 'rw', isa => ArrayRef, coerce => 1);
}

We have an items attribute that is constrained to the type ArrayRef. But we allow possible coercions to this type. If we now instantiate the class like this:

my $object = MyClass->new(items => int(rand 10) + 1);

We have completely unpredictable behaviour. Note that I only use the rand here as example of a source of random integers. A more realistic example could be:

my $object = MyClass->new(items => $dbic_object->id);

We probably intended to coerce to something like [$id], but depending on what the id is, we could also end up with [1 .. $id].

The Solution

So, what is the solution to this action at a distance? Since coercions aren’t inherited from parent types, the answer is simple. If we build our type library like this:

package MyLibrary;
use MooseX::Types -declare => [qw( Foo FooList )];
use MooseX::Types::Moose qw( ArrayRef );

subtype Foo, as Int, where { $_ > 0 };

subtype FooList, as ArrayRef;

coerce FooList, from Foo, via { [$_] };

1;

We can then use this types coercion without having to fear collisions with other opinions on how to get from an Int to an ArrayRef:

class MyClass {
    use MyLibrary qw( FooList );

    has items => (is => 'rw', isa => FooList, coerce => 1);
}

The above items attribute will only ever try coercions declared directly on the FooList type and won’t even attempt to do the coercion from the foreign Bar type constraint.

Note that I have side-skipped parameterized type to keep this illustration simple. Parameterized types are another, different topic for another day.

Class and Role types

Remember that I said that it is bad practice to declare coercions to all global types, including class types and role types? At first, it might seem that it could be handy to declare coercions on your own classes.

While I agree that the risks are by far smaller than with builtin types, simple subtyping of class types makes this small but existent risk even more unlikely. An often cited disadvantage of this strategy is that you “lose” parent coercions. Let’s subtype the Uri type constraint we can install from CPAN:

subtype MyUri, as Uri;

coerce MyUri, from Int, via { 
    URI->new(sprintf 'http://example.com/view/%i', $_);
};

Nice and easy, right? But we can’t coerce from simple strings containing URIs yet, like the original type could. This is because the coercions are not inherited. Don’t think of this as a missing feature though. Keeping coercions explicit makes sure a very large can full of pandora worms stays closed.

Personally, I wouldn’t be opposed to a declarative shortcut allowing explicit inheriting of coercions. A possible (still fictional) syntax might look like this:

# not implemented yet, inherits all coercions
coerce MyUri, like Uri;

# not implemented yet, inherits only some coercions
coerce MyUri, like Uri, from Str, from HashRef;

If someone cares enough, this could easily be implemented as a MooseX extension. Currently, we can do it manually:

coerce MyUri, 
  from Str | HashRef | ScalarRef, 
   via { to_Uri $_ };

This uses the to_Uri coercion helper exported by the URI type library to do a coercion to an URI object on the selected types.

In Conclusion

Type coercions are a very powerful tool for simplifying interfaces dramatically. But they require the same amount of thought and planning as the rest of software development. But if you go that bit of extra way to make sure your type and coercion logic is properly designed and encapsulated, this can have huge advantages. By being declarative and introspectable, with types and coercions, it is easy to build a consistent API throughout your project.

As usual, comments and feedback are greatly appreciated.

No TrackBacks

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

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