Type::Tiny and deep coercions
Asked Answered
U

1

11

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.

Unapproachable answered 10/4, 2014 at 7:40 Comment(2)
InstanceOf certainly does have plus_coercions. However, doing InstanceOf["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(...).Peanuts
Aaah, of course. Anyway, using a class_type lead to nice and tidy code. A ArrayRef[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
P
5

Moose/Moo/Mouse attributes don't coerce by default, so even though the type constraint has a coercion, you need to tell the attribute to use that coercion!

If you were using Moose or Mouse, you could do:

has members => (
  is     => 'ro',
  isa    => ArrayRef[ Object->plus_coercions(Str, q{ Person->new(name => $_) }) ],
  coerce => 1,
);

But Moo doesn't support coerce=>1; instead it expects a coderef or overloaded object to act as the coercion. Type::Tiny can provide you with a suitable overloaded object by calling $type->coercion.

# Let's store the type constraint in a variable to avoid repetition...
my $type = ArrayRef[
  Object->plus_coercions(  Str, q{Person->new(name => $_)}  )
];

has members => (
  is     => 'ro',
  isa    => $type,
  coerce => $type->coercion,
);
Peanuts answered 10/4, 2014 at 18:24 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.