Related
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',
);
I have a big hash many levels deep, and I'd like to turn this hash into a set of Moose classes.
The hash looks something like this:
my %hash = (
company => {
id => 1,
name => 'CorpInc',
departments => [
{
id => 1,
name => 'Sales',
employees => [
{
id => 1,
name => 'John Smith',
age => '30',
},
],
},
{
id => 2,
name => 'IT',
employees => [
{
id => 2,
name => 'Lucy Jones',
age => '28',
},
{
id => 3,
name => 'Miguel Cerveza',
age => '25',
},
],
},
],
}
);
And the Moose classes:
package Company;
use Moose;
has 'id' => (is => 'ro', isa => 'Num');
has 'name' => (is => 'ro', isa => 'Str');
has 'departments' => (is => 'ro', isa => 'ArrayRef[Company::Department]');
1;
package Company::Department;
use Moose;
has 'id' => (is => 'ro', isa => 'Num');
has 'name' => (is => 'ro', isa => 'Str');
has 'employees' => (is => 'ro', isa => 'ArrayRef[Company::Person]');
1;
package Company::Person;
use Moose;
has 'id' => (is => 'ro', isa => 'Num');
has 'first_name' => (is => 'ro', isa => 'Str');
has 'last_name' => (is => 'ro', isa => 'Str');
has 'age' => (is => 'ro', isa => 'Num');
1;
Whats the best way to turn this hash into a Company object?
The options I've considered so far are:
Manually loop the %hash, find the deepest "classes" (e.g Person), create these first, then manually add these to the newly created higher level classes (Department), and so on.
Add some kind of coercion functionality to each class, which lets me do something like Company->new(%hash), and make each class create its own "subclasses" (via coercion)
Convert the %hash into a structure similar to what MooseX::Storage would serialize to, then use MooseX::Storage to instatiate everything for me...
Any other ideas or suggestions?
You could have a BUILDARGS handler which converts unblessed references in those slots to objects. Coercions is probably the best, but it takes more doing. (Unless this is all coming from a RDBMS, in which case use DBIx::Class).
#!/usr/bin/env perl
use warnings;
use strict;
package Company;
use Moose;
has 'id' => (is => 'ro', isa => 'Num');
has 'name' => (is => 'ro', isa => 'Str');
has 'departments' => (is => 'ro', isa => 'ArrayRef[Company::Department]');
sub BUILDARGS {
my $self = shift;
my $args = $self->SUPER::BUILDARGS(#_);
#{ $args->{departments} } =
map { eval{ $_->isa('Company::Department') } ? $_ : Company::Department->new($_) }
#{ $args->{departments} };
return $args;
};
package Company::Department;
use Moose;
has 'id' => (is => 'ro', isa => 'Num');
has 'name' => (is => 'ro', isa => 'Str');
has 'employees' => (is => 'ro', isa => 'ArrayRef[Company::Person]');
sub BUILDARGS {
my $self = shift;
my $args = $self->SUPER::BUILDARGS(#_);
#{ $args->{employees} } =
map { eval{ $_->isa('Company::Person') } ? $_ : Company::Person->new($_) }
#{ $args->{employees} };
return $args;
};
package Company::Person;
use Moose;
has 'id' => (is => 'ro', isa => 'Num');
has 'name' => (is => 'ro', isa => 'Str');
has 'age' => (is => 'ro', isa => 'Num');
package main;
my %hash = (
company => {
id => 1,
name => 'CorpInc',
departments => [
{
id => 1,
name => 'Sales',
employees => [
{
id => 1,
name => 'John Smith',
age => '30',
},
],
},
{
id => 2,
name => 'IT',
employees => [
{
id => 2,
name => 'Lucy Jones',
age => '28',
},
{
id => 3,
name => 'Miguel Cerveza',
age => '25',
},
],
},
],
}
);
my $company = Company->new($hash{company});
use Data::Dumper;
print Dumper $company;
I've used your option 2 several times and it worked fine for me. Last instance was inflating JIRA REST API results into real objects. Note that with coercions you can also lookup an existing instance by id and create only if it does not exist.
Edit: Here is some code to demonstrate those coercions:
package Company::Types;
use Moose::Util::TypeConstraints;
subtype 'Company::Departments', as 'ArrayRef[Company::Department]';
coerce 'Company::Departments', from 'ArrayRef', via {
require Company::Department;
[ map { Company::Department->new($_) } #$_ ]
};
subtype 'Company::Persons', as 'ArrayRef[Company::Person]';
coerce 'Company::Persons', from 'ArrayRef', via {
require Company::Person;
[ map { Company::Person->new($_) } #$_ ]
};
no Moose::Util::TypeConstraints;
and in those classes:
use Company::Types;
has 'departments' => (is => 'ro', isa => 'Company::Departments', coerce => 1);
has 'employees' => (is => 'ro', isa => 'Company::Persons', coerce => 1);
then you can pass whole structure into Company constructor and all gets inflated properly.
In trying to answer How to instantiate Moose classes from a big hash, I think I have hit another place where I don't fully understand Moose type coercions. For some reason, the below code issues warnings:
You cannot coerce an attribute (departments) unless its type (ArrayRef[Company::Department]) has a coercion at ./test.pl line 12.
You cannot coerce an attribute (employees) unless its type (ArrayRef[Company::Person]) has a coercion at ./test.pl line 23.
but then succeeds.
#!/usr/bin/env perl
use warnings;
use strict;
package Company;
use Moose;
use Moose::Util::TypeConstraints;
has 'id' => (is => 'ro', isa => 'Num');
has 'name' => (is => 'ro', isa => 'Str');
has 'departments' => (is => 'ro', isa => 'ArrayRef[Company::Department]', coerce => 1);
coerce 'ArrayRef[Company::Department]',
from 'ArrayRef[HashRef]',
via { [ map { Company::Department->new($_) } #$_ ] };
package Company::Department;
use Moose;
has 'id' => (is => 'ro', isa => 'Num');
has 'name' => (is => 'ro', isa => 'Str');
has 'employees' => (is => 'ro', isa => 'ArrayRef[Company::Person]', coerce => 1);
package Company::Person;
use Moose;
use Moose::Util::TypeConstraints;
has 'id' => (is => 'ro', isa => 'Num');
has 'name' => (is => 'ro', isa => 'Str');
has 'age' => (is => 'ro', isa => 'Num');
coerce 'ArrayRef[Company::Person]',
from 'ArrayRef[HashRef]',
via { [ map { Company::Person->new($_) } #$_ ] };
package main;
my %hash = (
company => {
id => 1,
name => 'CorpInc',
departments => [
{
id => 1,
name => 'Sales',
employees => [
{
id => 1,
name => 'John Smith',
age => '30',
},
],
},
{
id => 2,
name => 'IT',
employees => [
{
id => 2,
name => 'Lucy Jones',
age => '28',
},
{
id => 3,
name => 'Miguel Cerveza',
age => '25',
},
],
},
],
}
);
my $company = Company->new($hash{company});
use Data::Dumper;
print Dumper $company;
How should this have been done? P.S. I tried simply doing
coerce 'Company::Department',
from 'HashRef',
via { Company::Department->new($_) };
but it died horribly.
Well, it doesn't succeed completely, and you should feel it when you'll try to update these fields with coerce => 1. That's why:
You cannot pass coerce => 1 unless the attribute's type constraint has
a coercion
Previously, this was accepted, and it sort of worked,
except that if you attempted to set the attribute after the object was
created, you would get a runtime error.
Now you will get an error when you attempt to define the attribute.
Still, I think I find the way to fix it, by introducing subtypes, first, and changing the order of packages, second:
package Company::Person;
use Moose;
use Moose::Util::TypeConstraints;
subtype 'ArrayRefCompanyPersons',
as 'ArrayRef[Company::Person]';
coerce 'ArrayRefCompanyPersons',
from 'ArrayRef[HashRef]',
via { [ map { Company::Person->new($_) } #$_ ] };
has 'id' => (is => 'ro', isa => 'Num');
has 'name' => (is => 'ro', isa => 'Str');
has 'age' => (is => 'ro', isa => 'Num');
package Company::Department;
use Moose;
has 'id' => (is => 'ro', isa => 'Num');
has 'name' => (is => 'ro', isa => 'Str');
has 'employees' => (is => 'ro', isa => 'ArrayRefCompanyPersons', coerce => 1);
package Company;
use Moose;
use Moose::Util::TypeConstraints;
subtype 'ArrayRefCompanyDepartments',
as 'ArrayRef[Company::Department]';
coerce 'ArrayRefCompanyDepartments',
from 'ArrayRef[HashRef]',
via { [ map { Company::Department->new($_) } #$_ ] };
has 'id' => (is => 'ro', isa => 'Num');
has 'name' => (is => 'ro', isa => 'Str');
has 'departments' => (is => 'ro', isa => 'ArrayRefCompanyDepartments', coerce => 1);
The rest of the code is the same as in your version. This works without any warnings, and more-o-less behaves like (again, I think) it should be.
From Moose::Manual::Type docs:
LOAD ORDER ISSUES
Because Moose types are defined at runtime, you may run into load order problems. In particular, you may want to use a class's type constraint before that type has been defined.
In order to ameliorate this problem, we recommend defining all of your custom types in one module, MyApp::Types, and then loading this module in all of your other modules.
So to add to raina77ow subtype & package order answer (+1) I would recommend creating a Company::Types module:
package Company::Types;
use Moose;
use Moose::Util::TypeConstraints;
subtype 'CompanyDepartments'
=> as 'ArrayRef[Company::Department]';
subtype 'CompanyPersons'
=> as 'ArrayRef[Company::Person]';
coerce 'CompanyDepartments'
=> from 'ArrayRef[HashRef]'
=> via {
require Company::Department;
[ map { Company::Department->new($_) } #$_ ];
};
coerce 'CompanyPersons'
=> from 'ArrayRef[HashRef]'
=> via { require Company::Person; [ map { Company::Person->new($_) } #$_ ] };
1;
And then put use Company::Types in all your Company:: classes.
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.
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$/ ]);