Type::Tiny and deep coercions - perl

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.

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,
);

Related

perl Mouse set value before object returned to user

I'm trying to write a perl module using Mouse, and after the object has been initialized, but before the user makes any calls, I need to initialize object1 with the two values from object2 and object3 that the user was required to give. I tried to use Mouse's after feature to have a subroutine called after new:
package Test;
use strict;
use Mouse;
has 'object1' => ( is => 'rw', isa => 'Any');
has 'object2' => ( is => 'ro', isa => 'Str', required => 1);
has 'object3' => ( is => 'ro', isa => 'Str', required => 1);
after 'new' => sub {
my ($self) = #_;
$self->object1(#do stuff with object2 and object3);
};
1;
However, currently I get this error:
Invalid object instance: 'Test' at lib/Test.pm line 18.
Is there a way I can initialize a value with user supplied values before the user gets the object reference returned to them?
Mouse is compatible with Moose. Object creation has the following phases:
Arguments are passed through the BUILDARGS method if one is defined. This can munge the arguments before Moose/Mouse touch them, e.g. to provide default arguments or to accommodate other calling styles than the keyword convention.
A new instance is created and the fields (declared by has) are populated.
The object is now set up and ready to use (from Mouse's/Moose's perspective). You might have a different idea of a correct setup, and can perform your checks in a BUILD method. This is the point where you can also perform remaining initialization that can't be expressed with has declarations.
So your example might become:
use strict;
use warnings;
package Your::Class;
use Mouse;
has 'object1' => ( is => 'rw', isa => 'Any');
has 'object2' => ( is => 'ro', isa => 'Str', required => 1);
has 'object3' => ( is => 'ro', isa => 'Str', required => 1);
sub BUILD {
my ($self) = #_;
$self->object1($self->object2 . $self->object3);
};
package main;
use Test::More;
# Your::Class->new(object2 => "foo", object3 => "bar");
my $instance = new_ok('Your::Class', [object2 => "foo", object3 => "bar"]);
is($instance->object1, "foobar");
done_testing;
To learn more about object construction in Moose and Moose-compatible object systems, read Moose::Manual::Construction.

Correct way define and convert the Moose attribute type

Have:
package MyPath;
use strict;
use warnings;
use Moose;
has 'path' => (
is => 'ro',
isa => 'Path::Class::Dir',
required => 1,
);
1;
But want create this object with two ways, like:
use strict;
use warnings;
use MyPath;
use Path::Class;
my $o1 = MyPath->new(path => dir('/string/path')); #as Path::Class::Dir
my $o2 = MyPath->new(path => '/string/path'); #as string (dies - on attr type)
And when call it with a 'Str' - want convert it internally in the MyPath package to Class::Path::Dir, so, both: $o1->path and $o2->path should return blessed Path::Class::Dir
When I tried extend the definition to the next:
has 'path' => (
is => 'ro',
isa => 'Path::Class::Dir|Str', #allowing both attr types
required => 1,
);
It doesn't works and still need "somewhat" convert the Str to Path::Class::Dir automatically-internally in the package MyPath...
Could someone give me some hints?
EDIT: Based on Oesor's hint I found than i need someting like:
coerce Directory,
from Str, via { Path::Class::Dir->new($_) };
has 'path' => (
is => 'ro',
isa => 'Directory',
required => 1,
);
But still havent idea how to correctly use it...
Some more hints please?
You're looking for type coersion.
use Moose;
use Moose::Util::TypeConstraints;
use Path::Class::Dir;
subtype 'Path::Class::Dir',
as 'Object',
where { $_->isa('Path::Class::Dir') };
coerce 'Path::Class::Dir',
from 'Str',
via { Path::Class::Dir->new($_) };
has 'path' => (
is => 'ro',
isa => 'Path::Class::Dir',
required => 1,
coerce => 1,
);
Hint -- look for how to coerce the value:
https://metacpan.org/pod/Moose::Manual::Types

Moose empty value for typed attributes

Is there some way in Moose to specify that I want an attribute to have a specific type, but also allow there to be a null value (undef?).
For example I am writing a simple implementation of a Linked List and have a Node class where the next and prev pointers are required to be of type Node (this is probably what you would expect)
package Node;
{
use Moose;
has 'value' => (
is => 'rw',
isa => 'Any', # Nodes can be of any type
);
has 'prev' => (
is => 'rw',
isa => 'Node',
predicate => 'has_prev',
);
has 'next' => (
is => 'rw',
isa => 'Node',
predicate => 'has_next',
);
}
But I was hoping to use a sentinel, empty node stored at the head of the list to mark the head, instead of an actual element of the list. So a list of elements [1, 2, 3] would actually look like:
EMPTY -> 1 -> 2 -> 3
I was hoping to be able to specify a empty value (like undef) for the next and prev pointers, but when I create an empty Node in my List class:
package List;
{
use Moose;
has 'head' => (
is => 'rw',
isa => 'Node',
# empty head node
default => sub {
Node->new( value => undef, next => undef, prev => undef );
},
);
Moose complains because undef is not of type Node.
Is there a way around this ?
You can use the Maybe[type] syntax to allow the type or undef. For your example:
has 'head' => (
is => 'rw',
isa => 'Maybe[Node]',
# empty head node
default => sub {
Node->new( value => undef, next => undef, prev => undef );
}
);
The next:
use 5.014;
use warnings;
package Node {
use Moose;
has 'value' => ( is => 'rw');
has 'prev' => ( is => 'rw', isa => 'Undef|Node', predicate => 'has_prev', default=>undef );
has 'next' => ( is => 'rw', isa => 'Undef|Node', predicate => 'has_next', default=>undef );
}
package List {
use Moose;
has 'head' => ( is => 'rw', isa => 'Node', default => sub { Node->new() } );
}
package main;
use Data::Dumper;
my $list = List->new();
say Dumper $list;
prints:
$VAR1 = bless( {
'head' => bless( {
'next' => undef,
'prev' => undef
}, 'Node' )
}, 'List' );
The Moose::Manual::Types says for the basic hier:
Undef <---- undefined
Defined
Value
Str
Num
Int
ClassName <---- Class name
RoleName
and later in the section TYPE UNIONS says:
Moose allows you to say that an attribute can be of two or more
disparate types. For example, we might allow an Object or FileHandle:
has 'output' => (
is => 'rw',
isa => 'Object | FileHandle', );
As others already says, here is a Maybe[Something] too, I haven't idea what is better, but the Something | SomethingOther looks more "perlish" (IMHO). ;)
The authors prefer Undef|Node over Maybe[Node].
has 'prev' => (
is => 'rw',
isa => 'Undef|Node',
predicate => 'has_prev',
);

Lazy Attribute Coercion

With Moose, you can have lazy builders on attributes, where the builder is called when the attribute is first accessed if the attribute was not already populated. You can have type coercion of an attribute with coerce, but this is applied whenever the attribute is set, so even on object initialization.
I'm looking for a way to implement lazy coercion, where an attribute may be initially populated, but is only coerced when it is first accessed. This is important when coercion is expensive.
In the following example, I use a union type and method modifiers to do this:
package My::Foo;
use Moose;
has x => (
is => 'rw',
isa => 'ArrayRef | Int',
required => 1
);
around "x" => sub {
my $orig = shift;
my $self = shift;
my $val = $self->$orig(#_);
unless(ref($val)) {
# Do the cocerion
$val = [ map { 1 } 1..$val ];
sleep(1); # in my case this is expensive
}
return $val;
};
1;
my $foo = My::Foo->new( x => 4 );
is_deeply $foo->x, [ 1, 1, 1, 1 ], "x converted from int to array at call time";
However there are a few problems with this:
I dislike the union type + method modifier approach. It goes against the "Best Practices" suggestion to use coercion instead of unions. It isn't declarative.
I need to do this with many attributes across many classes. Therefore some form of DRY is needed. This could be meta-attribute roles, type-coercion, what have you.
Update:
I followed ikegami's suggestion to encapsulate the expensive type coercion inside an object and provide an outer coercion to this object:
package My::ArrayFromInt;
use Moose;
use Moose::Util::TypeConstraints;
subtype 'My::ArrayFromInt::Inner',
as 'ArrayRef[Int]';
coerce 'My::ArrayFromInt::Inner',
from 'Int',
via { return [ (1) x $_ ] };
has uncoerced => (is => 'rw', isa => 'Any', required => 1);
has value => (
is => 'rw',
isa => 'My::ArrayFromInt::Inner',
builder => '_buildValue',
lazy => 1,
coerce => 1
);
sub _buildValue {
my ($self) = #_;
return $self->uncoerced;
}
1;
package My::Foo;
use Moose;
use Moose::Util::TypeConstraints;
subtype 'My::ArrayFromInt::Lazy' => as class_type('My::ArrayFromInt');
coerce 'My::ArrayFromInt::Lazy',
from 'Int',
via { My::ArrayFromInt->new( uncoerced => $_ ) };
has x => (
is => 'rw',
isa => 'My::ArrayFromInt::Lazy',
required => 1,
coerce => 1
);
1;
This works if $foo->x->value is called. However this doesn't solve point #2, as I would need to create My::ArrayFromInt and the ::Lazy subtype for each attribute I would like to transform. And I'd like to avoid calling $foo->x->value if possible.
How about having the typedef along the lines described, then doing
has _x => (
is => 'ro',
isa => 'Int|MyArrayOfInts',
init_arg => 'x',
required => 1,
);
has x => (
is => 'ro',
lazy => 1,
isa => 'MyArrayOfInts',
coerce => 1,
default => sub { $_[0]->_x },
);
It'd make sense to wrap that up into some kind of helper method to create the pair of objects along the lines of
has_lazily_coerced x => (
is => 'ro',
isa => 'TargetType',
);
which would introspect on TargetType to get a list of legal types for the uncoerced shadow attribute and generate the pair of attributes for you.

Apply specific check (beyond Moose types) to Moose attribute

Moose types are great, but sometimes you need to be more specific. You all know these data type rules: that parameter may only be 'A', 'B' or 'C', or only a currency symbol, or must conform to some regular expression.
Take a look at the following example which has two constrained attributes, one must be either 'm' or 'f', the other must be a valid ISO date. What's the best way in Moose to specify these constraints? I'd think of the SQL CHECK clause, but AFAICS there is no check keyword in Moose. So I used trigger, but it sounds wrong. Anyone has a better answer?
package Person;
use Moose;
has gender => is => 'rw', isa => 'Str', trigger =>
sub { confess 'either m or f' if $_[1] !~ m/^m|f$/ };
has name => is => 'rw', isa => 'Str';
has dateOfBirth => is => 'rw', isa => 'Str', trigger =>
sub { confess 'not an ISO date' if $_[1] !~ m/^\d\d\d\d-\d\d-\d\d$/ };
no Moose;
__PACKAGE__->meta->make_immutable;
package main;
use Test::More;
use Test::Exception;
dies_ok { Person->new( gender => 42 ) } 'gender must be m or f';
dies_ok { Person->new( dateOfBirth => 42 ) } 'must be an ISO date';
done_testing;
Here's what I wound up using:
package Blabla::Customer;
use Moose::Util::TypeConstraints;
use Moose;
subtype ISODate => as 'Str' => where { /^\d\d\d\d-\d\d-\d\d$/ };
has id => is => 'rw', isa => 'Str';
has gender => is => 'rw', isa => enum ['m', 'f'];
has firstname => is => 'rw', isa => 'Str';
has dateOfBirth => is => 'rw', isa => 'ISODate';
no Moose;
__PACKAGE__->meta->make_immutable;
This is Moose version 1.19, in case it matters. I got the following warning for the wrong subtype as => 'Str', where => { ... } syntax I erroneously introduced: Calling subtype() with a simple list of parameters is deprecated. So I had to change it a bit according to the fine manual.
Just define your own subtype, and use that.
package Person;
use Moose::Util::TypeConstraints;
use namespace::clean;
use Moose;
has gender => (
is => 'rw',
isa => subtype(
as 'Str',
where { /^[mf]$/ }
),
);
has name => (
is => 'rw',
isa => 'Str'
);
has dateOfBirth => (
is => 'rw',
isa => subtype(
as 'Str',
where { /^\d\d\d\d-\d\d-\d\d$/ }
),
);
no Moose;
__PACKAGE__->meta->make_immutable;
1;
package main;
use Test::More;
use Test::Exception;
dies_ok { Person->new( gender => 42 ) } 'gender must be m or f';
dies_ok { Person->new( dateOfBirth => 42 ) } 'must be an ISO date';
done_testing;
Or you could use the MooseX::Types module.
package Person::TypeConstraints;
use MooseX::Types::Moose qw'Str';
use MooseX::Types -declare => [qw'
Gender ISODate
'];
subtype Gender, (
as Str,
where { /^[mf]$/ },
);
subtype ISODate, (
as Str,
where { /^\d\d\d\d-\d\d-\d\d$/ }
);
1;
package Person:
use MooseX::Types::Moose qw'Str';
use Person::TypeConstraints qw'Gender ISODate';
use namespace::clean;
use Moose;
has gender => (
is => 'rw',
isa => Gender,
);
has name => (
is => 'rw',
isa => Str,
);
has dateOfBirth => (
is => 'rw',
isa => ISODate,
);
no Moose;
__PACKAGE__->meta->make_immutable;
1;
Adding your own type like Brad said:
use Moose::Util::TypeConstraints;
my $gender_constraint = subtype as 'Str', where { $_ =~ /^[FfMm]$/ };
has gender => ( is => 'rw', isa => $gender_constraint );
You could try using MooseX-Types-Parameterizable to implement types that take parameters for the cases you present (untested, just sketched):
package YourTypes;
use MooseX::Types -declare => [qw( OneOfStr MatchingStr )];
use MooseX::Types::Moose qw( Str ArrayRef RegexpRef );
subtype OneOfStr,
as Parameterizable[ Str, ArrayRef[ Str ] ],
where {
my ($str, $allowed) = #_;
return scalar grep { $_ eq $str } #$allowed;
};
subtype MatchingStr,
as Parameterizable[ Str, RegexpRef ],
where {
my ($str, $rx) = #_;
return scalar $str =~ $rx;
};
1;
and you would use it like this:
use YourTypes qw( OneOfStr MatchingStr );
has gender => (is => 'ro', isa => OneOfStr[ [qw( f m )] ]);
has dob => (is => 'ro', isa => MatchingStr[ qr/^$yourregex$/ ]);