Type::Tiny – not just for attributes

This is a very old article. It has been imported from older blogging software, and the formatting, images, etc may have been lost. Some links may be broken. Some of the information may no longer be correct. Opinions expressed in this article may no longer be held.

OK, so I’ve gotten back from the May Day parade, had some lunch, and now it’s time for me to write about Type::Tiny some more…

Type::Tiny is a zero-dependency implementation of type constraints that can be used with Moose, Mouse and Moo alike. (No more need to build separate type libraries for each of them!)

A typical usage might be:

        package Person;
        use Moose;
        use Types::Standard -types;
        
        has name => (is => "ro", isa => Str);
        has age  => (is => "ro", isa => Int);

But why stop there? Type constraints can also be useful for other purposes such as unit testing (e.g. check your function returns an Int) and validation. It’s validation we’ll look at today; specifically validating sub parameters.

Let’s take a look at a simple function which takes a hash, and returns a copy of that hash, but adding a number to certain keys.

        sub hash_add {
                my ($number, $hash, $keys) = @_;
                my %clone = %$hash;
                $clone{$_} += $number for @$keys;
                return \%clone;
        }
        
        my $r = hash_add(7, { foo => 1, bar => 2, baz => 3 }, [qw/foo bar/]);
        ## => { foo => 8, bar => 9, baz => 3 }

Why would you want a function like this? I admit it’s somewhat contrived, but it includes three different types (a number, a hashref and an arrayref), so is a good illustration of the principles involved.

Here’s how you’d add parameter validation using the venerable Params::Validate:

        use Params::Validate qw(:all);
        use Scalar::Util qw(looks_like_number);
        
        sub hash_add {
                my ($number, $hash, $keys) = validate_pos(@_,
                        {
                                type      => SCALAR,
                                callbacks => { numeric => sub { looks_like_number($_[0]) } },
                        },
                        { type => HASHREF },
                        { type => ARRAYREF },
                );
                ...;
        }

Using Type::Params which comes bundled with Type::Tiny is somewhat more elegant for this simple case (which is not to say that it is always so!):

        use Type::Params qw(compile);
        use Types::Standard -types;
        
        sub hash_add {
                state $check = compile(Num, HashRef, ArrayRef);
                my ($number, $hash, $keys) = $check->(@_);
                ...;
        }

But surely this elegance comes at some cost? After all; Params::Validate is fast! It has an XS backend that blows the socks off many of its rivals (Data::Validator, etc).

Well, you’d be wrong! According to my benchmarks, Type::Params is more than twice as fast as Params::Validate’s XS backend. (And more than six times as fast as Params::Validate’s pure Perl backend.) Don’t believe me? Here is my benchmark script.

In fact, Type::Params is so fast, and building up constraints is so simple, that you might find yourself wanting to make your validation more brutal, just because you can! Let’s make sure that all the values in the hashref are really numbers; and that all the elements of the arrayref are strings:

        sub hash_add {
                state $check = compile(Num, HashRef[Num], ArrayRef[Str]);
                my ($number, $hash, $keys) = $check->(@_);
                ...;
        }

OK, so this is slower than our earlier parameter check, but not unacceptably slow; and still faster than the less strict Params::Validate check.

How??

So, how does Type::Params achieve its speed? Super optimized assembly language programming linked to via XS? Nothing of the sort; it’s actually pure Perl.

It’s fast because the first time you call hash_add, it generates a long string of Perl code that will be used to validate your parameters, then passes that through eval to create a custom validation coderef for your sub. (Just a glimpse of the mess of source code within that coderef is enough to give many people nightmares!) This first call is actually far slower than Params::Validate – about 10 times slower than the PP backend, but that’s still under a millisecond on most modern computers.

Subsequent calls to the same function reuse that coderef, so go much faster.

The break-even point for using this trick seems to be around 20 sub calls. If your sub is going to be called more than 20 times, compiling that coderef is a sound investment. (If your sub is going to be called fewer times, then you probably don’t need to worry too much about micro-optimizing parameter validation anyway.)

This is similar to what Moose does when you run:

        __PACKAGE__->meta->make_immutable;

… and it’s used all over the place within the Type-Tiny distribution.

What else should I know?

Type::Params is not a drop-in replacement for Params::Validate. Their features overlap, but are not identical.

Params::Validate allows you to supply defaults for missing parameters; Type::Params does not. Params::Validate has a more natural interface for validating named parameters than Type::Params (though Type::Params can still do this). Each of them currently require Perl 5.8.1 or above, but CPAN still has old versions of Params::Validate available for Perl 5.5.

Type::Params automatically does coercion (including “deep coercions”) if your type constraint has coercions defined:

        use Type::Params qw(compile);
        use Type::Utils qw(declare as coerce from via);
        use Types::Standard qw(:types slurpy);
        
        my $Rounded = declare as Int;
        coerce $Rounded, from Num, via { int($_) };
        
        sub numbers {
                state $check = compile($Rounded, slurpy ArrayRef[$Rounded]);
                my ($first, $rest) = $check->(@_);
                
                # $first is 1
                # $rest is [2, 3]
        }
        
        numbers(1.1, 2.2, 3.3);

But for me, ultimately the most compelling reason to use Type::Params is that it allows you to use the same library of type constraints for sub parameter validation that you already use for attributes in Moose/Moo/Mouse OO code.