Exploring Type::Tiny Part 2: Using Type::Tiny with Moose

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 second in a series of posts showing other things you can use Type::Tiny for. You can refer back to part 1.

Type::Tiny is often used in Moo classes and roles as a drop-in replacement for Moose's built-in type system. But the original reason I wrote it was as a response to the growing number of MooseX::Types and MouseX::Types modules on CPAN. I thought "wouldn't it be good if you could write a type library once, and use it for Moose, Mouse, and maybe even Moo?" In the very early version, you needed to import types like this:

   use Type::Standard -moose, qw(Int);
   use Type::Standard -mouse, qw(Int);
   use Type::Standard -moo,   qw(Int);

Specifying which object system you were using allowed the type library to export different blessed type constraint objects for different object frameworks. Eventually this need was eliminated by having Type::Tiny's objects better mock the Moose and Mouse native APIs, so the frameworks didn't even notice you weren't using their built-in type constraints.

(While no longer documented, the -moose, etc import flags still work in all Type::Library-based type libraries.)

Anyway, so now you know Type::Tiny types can work with Moose, what are the reasons to use them over Moose's built-in type constraints?

Type::Tiny is Faster

In almost all cases, Type::Tiny checks and coercions run faster than the built-in Moose ones.

   use v5.16;
   use Benchmark qw(cmpthese);
   
   BEGIN {
      $ENV{PERL_TYPE_TINY_XS} = 0;
   }
   
   package Example::Native {
      use Moose;
      has numbers => (
         is       => 'rw',
         isa      => 'ArrayRef[Str]',
      );
      __PACKAGE__->meta->make_immutable;
   }
   
   package Example::TT {
      use Moose;
      use Types::Standard qw(ArrayRef Str);
      has numbers => (
         is       => 'rw',
         isa      => ArrayRef[Str],
      );
      __PACKAGE__->meta->make_immutable;
   }
   
   cmpthese -1, {
      native => q{
         my $obj = Example::Native->new(numbers => []);
         $obj->numbers([0 .. $_]) for 1 .. 50;
      },
      tt => q{
         my $obj = Example::TT->new(numbers => []);
         $obj->numbers([0 .. $_]) for 1 .. 50;
      },
   };
   
   __END__
            Rate native     tt
   native 2511/s     --   -45%
   tt     4525/s    80%     --

Note that even without XS, the Type::Tiny checks run 80% faster than Moose's native ones. If Type::Tiny::XS is available, it's about 400% faster. (Yeah, I could have tested ArrayRef[Int] but sadly the Int type is one of the slower type checks in Types::Standard, no faster than Moose.)

Type::Tiny has a Better Coercion Paradigm

In Moose, if you want to, say, coerce an arrayref of strings into a single string, then the usual way to do it is something like this:

   use Moose::Util::TypeConstraints;
   
   coerce 'Str',
      from 'ArrayRef', via { join "\n", @$_ };

However, this has a global effect. It doesn't just apply to string attributes in your class, but any string attributes which have coercion enabled for them.

While Type::Tiny does support globally defined coercions for Moose compatibility, the practice above, of adding your own coercions to types in standard libraries is strongly discouraged.

Instead, two routes to coercions are recommended.

Firstly, if you're making your own type library, feel free to define any useful coercions to the types in that library. Some of the type libraries bundled with Type::Tiny do include a few standard coercions. For example LowerCaseStr in Types::Common::String defines a coercion from non-empty strings (passing the string to Perl's lc function).

Secondly, if you're consuming types from a library (importing them into your role or class for use), don't add your own coercions to them. Instead, use the plus_coercions method.

   package MyClass {
      use Moose;
      use Types::Standard qw(ArrayRef Str);
      has data => (
         is     => 'ro',
         isa    => Str->plus_coercions(ArrayRef, sub { join "\n", @$_ }),
      );
   }

What does this do? Instead of adding coercions to the global definition of Str, it transparently creates a subtype of Str and adds your coercions to that.

There's also plus_fallback_coercions (which does the same thing but gives priority to any existing coercions the type constraint already has), minus_coercions (to remove particular existing coercions from a type), and no_coercions (to give you a blank slate).

Coercions can also be defined using strings of Perl code:

   Str->plus_coercions(ArrayRef, q{ join "\n", @$_ })

This allows them to be better optimized.

Type::Tiny Makes Subtyping a Breeze

   package MyClass {
      use Moose;
      use Types::Standard qw(Int);
      has even_number => (
         is     => 'ro',
         isa    => Int->where(sub { $_ % 2 == 0 }),
      );
   }

Need I say more?

Probably not.

But I'll add that again, you can use a string of Perl code to get slightly better performance.

Type::Tiny and MooseX::Types Interoperate Fine

   package MyClass {
      use Moose;
      use Types::Standard qw(ArrayRef);
      use MooseX::Types::Moose qw(Int);
      has will_this_work => (
         is     => 'ro',
         isa    => ArrayRef[Int],
      );
   }

Yeah, it works.

Validate Method Parameters

In part 1 of this series I described how you can use Type::Tiny type constraints to validate data passed to functions. If you're checking incoming data to your accessors and constructors, why not check parameters passed to method calls as well? Type::Params lets you use the same types and coercions you're familiar with from defining attributes to validate method parameters.

   use v5.16;
   
   package MyClass {
      use Moose;
      use Types::Standard qw(Object Int);
      use Type::Params qw(compile);
      
      my $EvenInt = Int->where(sub { $_ % 2 == 0 });
      
      has even_number => (
         is     => 'ro',
         isa    => $EvenInt,
         writer => '_set_even_number',
      );
      
      sub add_another_even {
         state $check = compile(Object, $EvenInt);
         my ($self, $n) = &$check;
         $self->_set_even_number( $self->even_number + $n );
         return $self;
      }
   }

Leave a comment

About Toby Inkster

user-pic I'm tobyink on CPAN, IRC and PerlMonks.