I'm trying to get deep coercions work with Type::Tiny
without any success. From the manual it's said that:
"Certain parameterized type constraints can automatically acquire
coercions if their parameters have coercions. For example:
ArrayRef[Int->plus_coercions(Num, q{int($_)}) ]
... does what you mean!"
What I try to accomplish is getting something like this "do what I mean":
package Person;
use Types::Standard -types;
use Moo;
has name => (is => 'ro', isa => Str);
package Family;
use Types::Standard -types;
use Moo;
has members => (is => 'ro', isa => ArrayRef[InstanceOf['Person']]);
package main;
my $family = Family->new(members => [
'mom',
Person->new(name => 'dad'),
Person->new(name => 'girl'),
'dog'
]);
When instantiating Family
with elements that are a Str
they should be automatically be coerced into Person
objects. I've tried a range of different ideas (plus_coercions, Type libraries, etc) without any luck. They all fail in the same way.
When using plus_coercions (from Str
to Object
)
package Family;
has members => (
is => 'ro',
isa => ArrayRef[ Object->plus_coercions(Str, q{ Person->new(name => $_) }) ],
);
Type::Tiny throws an exception:
Reference ["mom",bless( {"name" => "dad"}, 'Person' ),bless( {"name" =...] did not pass type constraint "ArrayRef[Object]" (in $args->{"members"})
"ArrayRef[Object]" constrains each value in the array with "Object"
"Object" is a subtype of "Object"
"Object" is a subtype of "Ref"
Value "mom" did not pass type constraint "Ref" (in $args->{"members"}->[0])
"Ref" is defined as: (!!ref($_))
I know I could get around this by modifying the arguments to Family->new
using a BUILDARGS
sub in Family
, but it would be neat if Type::Tiny could do that automatically.
Update
Thanks to Tobys friendly help, I got this working. The only part that troubled me a bit was the use of ArrayRef[Object]
instead of the correct ArrayRef[InstanceOf['Person']]
(InstanceOf
doesn't have any plus_coercions
). With Object
an instance of any class could have been inserted into members
, and that is certainly not what you want.
Got around that by making a class_type
. Here's the full working code:
package Person;
use Types::Standard -types;
use Moo;
has name => (is => 'ro', isa => Str);
package Family;
use Types::Standard -types;
use Type::Utils -all;
use Moo;
my $Person = class_type { class => 'Person' };
my $Members = ArrayRef[
$Person->plus_coercions(Str, q{ Person->new(name => $_) })
];
has members => (
is => 'ro',
isa => $Members,
coerce => $Members->coercion,
);
sub list { join(', ', map { $_->name } @{ $_[0]->members }) }
package main;
my $family = Family->new(members => [
'mom',
Person->new(name => 'dad'),
Person->new(name => 'girl'),
'dog'
]);
print $family->list, "\n";
Which nicely prints mom, dad, girl, dog
when run.
InstanceOf
certainly does haveplus_coercions
. However, doingInstanceOf["Person"]->plus_coercions(...)
will try to call a method on an unblessed arrayref because of Perl operator precedence. It can be sorted out by adding some parentheses:(InstanceOf["Person"])->plus_coercions(...)
. – Peanutsclass_type
lead to nice and tidy code. AArrayRef[InstanceOf['Class::Name::Which::Is::Very::Long']]
is quite unwieldy. But it's good to know more than one way to do it. As always! – Unapproachable