Type::Tiny::Manual::Coercions - adding coercions to type constraints
Stop! Don't do it!
OK, it's fairly common practice in Moose/Mouse code to define coercions for type constraints. For example, suppose we have a type constraint:
class_type PathTiny, { class => "Path::Tiny" };
We may wish to define a coercion (i.e. a conversion routine) to handle strings, and convert them into Path::Tiny objects:
coerce PathTiny, from Str, via { "Path::Tiny"->new($_) };
However, there are good reasons to avoid this practice. It ties the coercion routine to the type constraint. Any people wishing to use your PathTiny type constraint need to buy in to your idea of how they should be coerced from Str. With Path::Tiny this is unlikely to be controversial, however consider:
PathTiny
Str
coerce ArrayRef, from Str, via { [split /\n/] };
In one part of the application (dealing with parsing log files for instance), this could be legitimate. But another part (dealing with logins perhaps) might prefer to split on colons. Another (dealing with web services) might attempt to parse the string as a JSON array.
If all these coercions have attached themselves to the ArrayRef type constraint, coercing a string becomes a complicated proposition! In a large application where coercions are defined across many different files, the application can start to suffer from "spooky action at a distance".
ArrayRef
In the interests of Moose-compatibility, Type::Tiny and Type::Coercion do allow you to define coercions this way, but they also provide an alternative that you should consider: plus_coercions.
plus_coercions
Type::Tiny offers a method plus_coercions which constructs a new anonymous type constraint, but with additional coercions.
In our earlier example, we'd define the PathTiny type constraint as before:
But then not define any coercions for it. Later, when using the type constraint, we can add coercions:
my $ConfigFileType = PathTiny->plus_coercions( Str, sub { "Path::Tiny"->new($_) }, Undef, sub { "Path::Tiny"->new("/etc/myapp/default.conf") }, ); has config_file => ( is => "ro", isa => $ConfigFileType, coerce => 1, );
Where the PathTiny constraint is used in another part of the code, it will not see these coercions, because they were added to the new anonymous type constraint, not to the PathTiny constraint itself!
Stepping away from the flow of this article, I'll point out that the following also works, using strings of Perl code instead of coderefs. It allows Type::Coercion to do a little optimization and run faster:
my $ConfigFileType = PathTiny->plus_coercions( Str, q{ "Path::Tiny"->new($_) }, Undef, q{ "Path::Tiny"->new("/etc/myapp/default.conf") }, );
Now, where were we...?
A type library may define a named set of coercions to a particular type. For example, let's define that coercion from Str to ArrayRef:
declare_coercion "LinesFromStr", to_type ArrayRef, from Str, q{ [split /\n/] };
Now we can import that coercion using a name, and it makes our code look a little cleaner:
use Types::Standard qw(ArrayRef); use MyApp::Types qw(LinesFromStr); has lines => ( is => "ro", isa => ArrayRef->plus_coercions(LinesFromStr), coerce => 1, );
Type::Tiny and Type::Coercion overload the + operator to add coercions. So you may use:
+
isa => PathTiny + PathTinyFromStr,
However, beware precedence. The following is parsed as a function call with an argument preceded by a unary plus:
isa => ArrayRef + LinesFromStr, # ArrayRef( +LinesFromStr )
When things can be parameterized, it's generally a good idea to wrap them in parentheses to disambiguate:
isa => (ArrayRef) + LinesFromStr,
Parameterized type constraints are familiar from Moose. For example, an arrayref of integers:
ArrayRef[Int]
Type::Coercion supports parameterized named coercions too. For example, the following type constraint has a coercion from strings that splits them into lines:
use Types::Standard qw( ArrayRef Split ); my $ArrayOfLines = (ArrayRef) + Split[ qr{\n} ];
The implementation of this feature is considered experimental, and the API for building parameterized coercions is likely to change. However, the feature itself, and its surface syntax (the square brackets) is likely to stay. So beware building your own parameterizable coercions, but don't be shy about using the ones in Types::Standard.
Getting back to the plus_coercions method, there are some other methods that perform coercion maths.
plus_fallback_coercions is the same as plus_coercions but the added coercions have a lower priority than any existing coercions.
plus_fallback_coercions
minus_coercions can be given a list of type constraints that we wish to ignore coercions for. Imagine our PathTiny constraint already has a coercion from Str, then the following creates a new anonymous type constraint without that coercion:
minus_coercions
PathTiny->minus_coercions(Str)
no_coercions gives us a new type anonymous constraint without any of its parents coercions. This is useful as a way to create a blank slate for a subsequent plus_coercions:
no_coercions
PathTiny->no_coercions->plus_coercions(...)
The plus_constructors method defined in Type::Tiny::Class is sugar for plus_coercions. The following two are the same:
plus_constructors
PathTiny->plus_coercions(Str, q{ Path::Tiny->new($_) }) PathTiny->plus_constructors(Str, "new");
Certain parameterized type constraints can automatically acquire coercions if their parameters have coercions. For example:
ArrayRef[ Int->plus_coercions(Num, q{int($_)}) ]
... does what you mean!
The parameterized type constraints that do this magic include the following ones from Types::Standard:
ScalarRef
HashRef
Map
Tuple
Dict
Optional
Maybe
Consider the following type library:
{ package Types::Geometric; use Type::Library -base, -declare => qw( VectorArray VectorArray3D Point Point3D ); use Type::Utils; use Types::Standard qw( Num Tuple InstanceOf ); declare VectorArray, as Tuple[Num, Num]; declare VectorArray3D, as Tuple[Num, Num, Num]; coerce VectorArray3D, from VectorArray, via { [ @$_, 0 ]; }; class_type Point, { class => "Point" }; coerce Point, from VectorArray, via { Point->new(x => $_->[0], y => $_->[1]); }; class_type Point3D, { class => "Point3D" }; coerce Point3D, from VectorArray3D, via { Point3D->new(x => $_->[0], y => $_->[1], z => $_->[2]); }, from Point, via { Point3D->new(x => $_->x, y => $_->y, z => 0); }; }
Given an arrayref [1, 1] you might reasonably expect it to be coercible to a Point3D object; it matches the type constraint VectorArray so can be coerced to VectorArray3D and thus to Point3D.
[1, 1]
Point3D
VectorArray
VectorArray3D
However, Type::Coercion does not automatically chain coercions like this. Firstly, it would be incompatible with Moose's type coercion system which does not chain coercions. Secondly, it's ambiguous; in our example, the arrayref could be coerced along two different paths (via VectorArray3D or via Point); in this case the end result would be the same, but in other cases it might not. Thirdly, it runs the risk of accidentally creating loops.
Point
Doing the chaining manually though is pretty simple. Firstly, we'll take note of the coercibles method in Type::Tiny. This method called as VectorArray3D->coercibles returns a type constraint meaning "anything that can be coerced to a VectorArray3D".
coercibles
VectorArray3D->coercibles
So we can define the coercions for Point3D as:
coerce Point3D, from VectorArray3D->coercibles, via { my $tmp = to_VectorArray3D($_); Point3D->new(x => $tmp->[0], y => $tmp->[1], z => $tmp->[2]); }, from Point, via { Point3D->new(x => $_->x, y => $_->y, z => 0); };
... and now coercing from [1, 1] will work.
Moose::Manual::BestPractices, http://www.catalyzed.org/2009/06/keeping-your-coercions-to-yourself.html.
Toby Inkster <tobyink@cpan.org>.
This software is copyright (c) 2013 by Toby Inkster.
This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.
THIS PACKAGE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF MERCHANTIBILITY AND FITNESS FOR A PARTICULAR PURPOSE.
To install Type::Tiny, copy and paste the appropriate command in to your terminal.
cpanm
cpanm Type::Tiny
CPAN shell
perl -MCPAN -e shell install Type::Tiny
For more information on module installation, please visit the detailed CPAN module installation guide.