Exploring Type::Tiny Part 3: Using Type::Tie
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 third in a series of posts showing other things you can use Type::Tiny for. This article along with part 1 and part 2 can be found on my blog and in the Cool Uses for Perl section of PerlMonks.
This works:
use Types::Standard qw(Int); tie(my @numbers, Int); push @numbers, 1, 2, 3; # ok push @numbers, "four"; # dies
Well, if you try it, you may find it complains about not being able to load Type::Tie.
Type::Tie is an add-on for Type::Tiny distributed separately. It's an optional dependency, so if you want to use this feature, you'll need to make sure it's installed.
Coercions
This tie feature automatically supports coercions.
use Types::Standard qw(Int Num); my $RoundedInt = Int->plus_coercions( Num, 'int $_' ); tie(my @numbers, $RoundedInt); push @numbers, 1, 2, 3; # ok push @numbers, 4.2; # rounded to 4 push @numbers, "five"; # dies
More about Type::Tie
Type::Tie is designed to be pretty independent of Type::Tiny. You can use it with MooseX::Types, Mouse::Types, and Specio, and it also bundles its own nanoscale type constraint library Type::Nano.
use Type::Tie qw(); use MooseX::Types::Moose qw(Int); tie(my @numbers, "Type::Tie::ARRAY", Int);
To save yourself typing "Type::Tie::ARRAY", "Type::Tie::HASH", and "Type::Tie::SCALAR" all the time, Type::Tie offers a convenience function ttie
:
use Type::Tie qw(ttie); use MooseX::Types::Moose qw(Int); ttie(my @numbers, Int);
Use in Attributes
Perl has a type checking hole thanks to references:
use v5.16; package Foo { use Moo; use Types::Standard qw(ArrayRef Int); has numbers => ( required => 1, is => 'ro', isa => ArrayRef[Int], ); } my $foo = Foo->new( numbers => [1, 2, 3] ); push @{ $foo->numbers }, "hi"; # this is allowed
The type constraint is only checked in the constructor and in writers/accessors.
Tying the array allows you to perform type checks and coercions on any new elements added to the array. It's a use for trigger
that doesn't suck!
use v5.16; package Foo { use Moo; use Types::Standard qw(ArrayRef Int); has numbers => ( required => 1, is => 'ro', isa => ArrayRef[Int], trigger => sub { tie @{$_[1]}, Int }, ); } my $foo = Foo->new( numbers => [1, 2, 3] ); push @{ $foo->numbers }, "hi"; # dies
With a little bit of work (okay, a lot!) it should be possible to even check deeply nested structures.
Performance
While effort has been made to optimize Type::Tie, tied variables are necessarily slower than untied ones.
If you have an array you want to make sure only contains integers, but you don't want to compromise on performance, you could enable the tie only when you run your test suite, and trust that your test suite will be enough to trigger any potential errors.
use Types::Standard qw(Int); use Devel::StrictMode qw(STRICT); my @array_of_ints; tie @array_of_ints, Int if STRICT; ...; # do stuff here
Devel::StrictMode is a module which exports a constant called STRICT
which will be true if the PERL_STRICT, EXTENDED_TESTING, RELEASE_TESTING, or AUTHOR_TESTING environment variables is true, and false otherwise.
It would be really great to have the method/pattern of using trigger so that ArrayRef[Int] can prevent q(push @{ $foo->numbers }, "hi") documented in the perldoc for either Type::Tiny or Type::Tie. I've been struggling to getting this working forever, smelling that it should be close/easy.
Alas, in the example above "tie" removes the elements 1, 2, 3 from the numbers attribute, so $foo will have 0 elements after the constructor (contrary to what I would've expected).
ttie @{ $_[1] }, Int, @{ $_[1] };
instead of the tie line does seem to work, though.
(Also, the untied ArrayRef provided to the constructor is now bound to Int - something the caller may not expect or want.)
It would be great to have a sure-fire way of using Type::Tie with ArrayRef (and HashRef), so it works in Moose, Moo and Mouse and works with values from both constructor and setter.