Exploring Type::Tiny Part 7: Creating a Type Library with Type::Library

This is an old article. Some links may be broken. Some of the information may no longer be correct. Opinions expressed in this article may no longer be held.

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.