Exploring Type::Tiny Part 7: Creating a Type Library with Type::Library
Type::Tiny is probably best known as a way of having Moose-like type constraints in Moo, but it can be used for so much more. This is the seventh in a series of posts showing other things you can use Type::Tiny for. This article along with the earlier ones in the series can be found on my blog and in the Cool Uses for Perl section of PerlMonks.
For small projects, the type constraints in Types::Standard and other CPAN type libraries are probably enough to satisfy your needs. You can do things like:
use Types::Common::Numeric qw(PositiveInt); has user_id => ( is => 'ro', isa => PositiveInt, );
However for larger apps, say you need to check user identity numbers in an handful of places throughout your code and you use PositiveInt
everywhere, then if you ever feel the need to change the constraint for them, you'll need to hunt through your code to look for every use of PositiveInt
, make sure it's not being used for some other reason (like to check an age or a counter), and update it.
So it is helpful to make your own application-specific type library. You can define your own UserId
type constraint, and use that everywhere. If the format of your identifiers ever changes, you only need to change the definition of the type constraint.
Moose-Like Syntax
package MyApp::Types { use Type::Library -base, -declare => qw( UserId UserIdList ); use Type::Utils -all; BEGIN { extends qw( Types::Standard Types::Common::Numeric Types::Common::String ); }; declare UserId, as PositiveInt, where { $_ > 1000 }; declare UserIdList, as ArrayRef[UserId]; ...; }
Using -base
from Type::Library sets your package up as an exporter that inherits from Type::Library. Using -declare
allows the type constraints there to be written as barewords in the rest of the package. Importing from Type::Utils gives you a bunch of helpful keywords that can be useful for defining your type constraints. (These keywords will be pretty familiar to people who have defined their own type constraints in Moose or MooseX::Types, but personally I prefer not to use them. I'll show you how to write this type library without the keywords from Type::Utils later.)
The extends
statement imports all the type constraints from the given type libraries, so all those types are added to this library. Putting it in a BEGIN
block allows them to be written as barewords too.
And then we define a couple of type constraints. Hopefully that part is pretty self-explanatory. The declare
, as
, and where
keywords are some of the things exported by Type::Utils.
Now your application code can just do:
use MyApp::Types qw( UserId UserIdList HashRef NonEmptyStr );
Your type library is also the perfect place to define any application-wide type coercions. For example:
declare User, as InstanceOf['MyApp::User']; coerce User, from UserId, via { MyApp::Utils::find_user_by_id($_) }; coerce UserId, from User, via { $_->user_id };
Bare Bones Syntax
Although Type::Tiny supports this Moose-like syntax for defining type constraints, I personally find the Type::Utils DSL a little unnecessary. Here's another way you can write the same type library:
package MyApp::Types { use Type::Library -base; use Type::Utils (); # don't import any keywords BEGIN { # Type::Utils is still the easiest way to do this part! Type::Utils::extends(qw( Types::Standard Types::Common::Numeric Types::Common::String )); }; my $userid = __PACKAGE__->add_type({ name => 'UserId', parent => PositiveInt, constraint => '$_ > 1000', }); my $user = __PACKAGE__->add_type({ name => 'User', parent => InstanceOf['MyApp::User'], }); $userid->coercion->add_type_coercions( $user => '$_->user_id' ); $user->coercion->add_type_coercions( $userid => 'MyApp::Utils::find_user_by_id($_)', ); __PACKAGE__->add_type({ name => 'UserIdList', parent => ArrayRef[$userid], coercion => 1, }); ...; __PACKAGE__->make_immutable; }
Defining types this way exposes some parts of Type::Tiny which are subtly different from Moose. For example, coercions and contraints can be expressed as strings of Perl code. This allows Type::Tiny to optimize some of the Perl code it generates, avoiding the overhead of a function call. Notice also the coercion => 1
when defining UserIdList
. This allows UserIdList
to inherit ArrayRef's automatic ability to coerce one level deep.
Calling make_immutable
on the package allows Type::Coercion to further optimize coercions for all the types in the library and prevents code outside the library from changing the global coercions you've defined.
# Imagine this is some code in a class... # use MyApp::Types qw( UserId Str ); # This will die because UserId is immutable now. UserId->coercion->add_type_coercions(Str, sub { ... }); # This will work, and only affect this one attribute. has user_id => ( is => 'ro', isa => UserId->plus_coercions(Str, sub { ... }), coerce => 1, );
So this method of defining type libraries might look a little less clean, but it has advantages. And as I said, it's how I prefer to do things.
Defining Utility Functions
All Type::Library-based type libraries automatically inherit from Exporter::Tiny and can also be used to define utility functions. Just define a normal Perl sub in the package and add:
our @EXPORT_OK = qw( my_function_name );
I recommend using lower-case function names with underscores to separate words to make them visually distinct from camel-case type constraint names.
To avoid creating a confusing package with a mishmash of unrelated functions, this feature should probably only be used to export functions which are vaguely related to types — validation functions, coercion functions, etc.
Could you explain what this means?
Okay, so imagine there's a coercion from Int to User like this:
If you define an attribute like this:
Then you can pass an integer as the manager, and it will automatically be converted to a User object. Now imagine an attribute defined like this:
A feature of Type::Tiny which isn't found in the standard Moose type constraint system is that this will automatically use the coercion from Int to User. So you can pass an arrayref of integers and it will be converted to an arrayref of User objects. You can even pass a mixed arrayref containing some integers and some User objects.
However, say you define a type like this:
And then define your attribute like this:
Now the staff attribute doesn't automatically coerce from a list of integers. That's because
UserList
is not the same type constraint asArrayRef[User]
; it's a child type ofArrayRef[User]
. And child types don't automatically inherit coercions from parent types. (It wouldn't make sense for them to do so. Num is a child of Str, but a coercion to Str wouldn't necessarily result in a valid Num.)But Type::Tiny allows you to indicate that you would like
UserList
to inheritArrayRef[User]
's coercions, like this: