Introducing Type::Tiny

Type::Tiny is a tiny (no non-core dependencies) framework for building type constraints. OK, probably not that exciting. How can I grab your attention?

                         Rate            WithMoose WithMooseAndTypeTiny
 WithMoose             8071/s                   --                 -25%
 WithMooseAndTypeTiny 10778/s                  34%                   --

The benchmark script is shown later so you can check I'm not doing anything hideously unfair to disadvantage Moose.

How can I hold your attention?

Type constraint libraries built with Type::Tiny work with Moose, Mouse and Moo! And because it's such a lightweight framework, with no dependencies on heavy metaobject protocols, it even becomes appealing to use in situations where you might not otherwise consider using a type constraint library at all.

Type::Tiny comes bundled with a number of other modules that help round out the framework, including:

Type Constraints?

Let's get back to basics... what's a type constraint library? If you're writing anything more than a quick throwaway script, you generally need to do a bit of data validation. Your array_sum function might need to check that it gets passed an arrayref and all the values in the array are numeric.

In another part of the code your delete_articles_by_id function might also need to accept an array of numeric values. Two checks for arrayrefs of numbers, in different parts of your codebase. The principle of DRY says that you should factor both of these checks out to a single place in your code.

Once you've factored all of these checks out into one place, that's your type constraint library.

Building a Type Library with Type::Library

Let's say we want to build a "natural numbers" type constraint. Natural numbers are the positive integers plus zero. (The inclusion of zero is contentious in some circles, but we'll put that aside for now.) It helps that Types::Standard defines an Int type constraint, so rather than starting from scratch, we can refine that.

 package MyApp::Types;
 
 use base "Type::Library";  # type libraries must inherit from this
 use Type::Utils;           # sugar for declaring type constraints
 use Types::Standard qw(Int);
 
 declare "NaturalNum", as Int, where { $_ >= 0 };
 
 1;  # magic true value

That was easy. Now within our application we can:

 use MyApp::Types qw(NaturalNum);

And this will export NaturalNum as a "constant". The constant returns an object that we can call methods on, so:

 NaturalNum->check($value);         # returns true or false
 NaturalNum->assert_valid($value);  # returns true or dies

The constant can also be used directly within Moo or Moose attribute declarations:

 has message_count => (is => "ro", isa => NaturalNum, required => 1);

Coercions

A next step is to define coercions. Within our type constraint library we can add:

 use Types::Standard qw( Num ArrayRef );
 
 coerce "NaturalNum",
    from Num,      via { int(abs($_)) },
    from ArrayRef, via { scalar(@$_) };

Now within our application we can:

 use MyApp::Types qw(to_NaturalNum);
 
 my $goats = ["Alice Gruff", "Bob Gruff", "Carol Gruff"];
 say to_NaturalNum($goats);  # say 3

Coercions can be used within Moose attribute definitions:

 has message_count => (
    is       => "ro",
    isa      => NaturalNum,
    required => 1,
    coerce   => 1,
 );

Or Moo attribute definitions:

 has message_count => (
    is       => "ro",
    isa      => NaturalNum,
    required => 1,
    coerce   => NaturalNum->coercion,  # spot the difference
 );

Coercions are a useful feature, and there are planned additions to Type::Coercion and Type::Library to make them even better in the future.

* * *

Anyway, I hope this provides a brief summary of Type::Tiny's features, and maybe tempts you to try it out. Keep an eye out for future articles on topics such as optimizing type constraints, and coercion power features.

Appendix

Here's the benchmarking script as promised...

 package main;
 use strict;
 use warnings;
 use Benchmark qw(cmpthese);
 
 {
    package Class::WithMoose;
    use Moose;
    has attr => (is => "ro", isa => "ArrayRef[Int]");
    __PACKAGE__->meta->make_immutable;
 }
 
 {
    package Class::WithMooseAndTypeTiny;
    use Moose;
    use Types::Standard -all;
    has attr => (is => "ro", isa => ArrayRef[Int]);
    __PACKAGE__->meta->make_immutable;
 }
 
 our %data = ( attr => [1 .. 20] );
 
 cmpthese(-1, {
    WithMoose            => q{ Class::WithMoose->new(%::data) },
    WithMooseAndTypeTiny => q{ Class::WithMooseAndTypeTiny->new(%::data) },
 });

5 Comments

I wonder if these will work with Test::Deep::Type?

Cool!

I think there's a feeling now that a global type registry is a bad idea, or at least has enough drawbacks that it might not always be a good idea -- different modules can stomp on each other by altering the global registry. Certainly the approach MooseX::Types has taken (that you are following here), where each type must be referred to directly (e.g. via an imported sub that returns the type object) has proven to work out well.

What would be even better is if type objects were immutable, to prevent similar stomping (e.g. someone adding a coercion to an existing type) -- so if one wants to make modifications, one has to create a *new* object, rather than modifying an existing type that an imported sub returns.

All my +1s. This is awesome. Thanks for sharing!

Leave a comment

About Toby Inkster

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