Type::Tiny - not just for attributes
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.
That's quite nice. I tried using it in Attribute::Contract and got smth like this:
sub method :ContractRequires(ClassName, Str) {
}
Works like a charm :)
Attribute::Contract looks interesting. I've shied away from adding sub attribute declaration (or something like Method::Signatures-style declaration) from Type::Params so far, because I've been unable to find a pleasant way to map string type names (e.g. "Str") to the underlying type constraint objects. (This is the one thing Moose's global type constraint registry is good for!) If you've found a good solution for that, I look forward to seeing it.
One feature you may find useful is this:
... so you can stuff all the type constraints that a package needs into a hash, and use them like that. I don't know how useful that might be.
Oh. Using a hashref might be better, than using eval (that's what I'm doing now :)! But using eval is so simple. This is how it looks inside:
...here goes types importing...
my $check = eval "Type::Params::compile($attributes)";
Where $attributes can be anything, really. No need for a special string convertion or anything like that. Just use normal Type declarations.
Why I chose attributes for contracts:
1) No need for same code duplicated over classes
2) I can make them 'inheritable' (this is a HUGE win for me, like you can write an interface and then just implement it in various classes without the need to repeat sub signatures)
3) Readable and don't get in your way when you're trying to understand what the code does
4) You can turn them off
5) Executed during the compile time, so all the checks are precompiled
And I like contracts more than sub signatures because you can specify the checks for return values too.
Some of your example code is cut off on the right side. Can you fix that please?