MooseX::RelClassTypes - specify a class name in an attribute isa relative to the current class
isa
package Dog; use Moose; with 'MooseX::RelClassTypes'; has tail => ( is => 'rw', isa => 'Tail::{CLASS}' # sets constraint as 'Tail::Dog' ); package Cat; use Moose; with 'MooseX::RelClassTypes' has tail => ( is => 'rw', isa => 'Tail::{CLASS}' # sets constraint as 'Tail::Cat' );
To group accessors it can be convenient to create a nested structure of Moose objects. For example, instead of having
package Car; use Moose; has max_speed => (is => 'rw', isa => 'Int'); has max_acceleration => (is => 'rw', isa => 'Int'); has turning_circle => (is => 'rw', isa => 'Int'); has height => (is => 'ro', isa => 'Int'); has weight => (is => 'ro', isa => 'Int'); has length => (is => 'ro', isa => 'Int'); has color => (is => 'ro', isa => 'Str'); has style => (is => 'ro', isa => 'Str'); has seat_fabric => (is => ro', isa => 'Str'); # ... other methods
you could group the attributes in some convenient way:
package Car use Moose; has performance => ( is => 'rw', isa => 'Car::Performance' ); has static_properties => ( is => 'rw', isa => 'Car::Properties' ); has appearance => ( is => 'rw', isa => 'Car::Appearance' ); # ... other methods
And Car::Performance would look like
package Car::Performance use Moose; has max_speed => ( is => 'rw', isa => 'Int' ); has max_acceleration => ( is => 'rw', isa => 'Int' ); has turning_circle => ( is => 'rw', isa => 'Int' );
with Car::Properties and Car::Appearance organised similarly. Then if you have an application which is only interested in performance (say) then your app can manipulate Car::Performance without having to load the whole car.
Car::Properties
Car::Appearance
Car::Performance
What if we have a truck?
package Truck; use Moose; has performace => ( is => 'rw', isa => 'Truck::Performance' ); has static_properties => ( is => 'rw', isa => 'Truck::Properties' ); has appearance => ( is => 'rw', isa => 'Truck::Appearance' );
This looks a lot like the Car package, but with Car replaced by Truck throughout. Now of course inheritance is the way to go in this situation:
Car
Truck
package Vehicle; use Moose; has performance => ( is => 'rw', isa => 'Vehicle::Performance' ); has static_properties => ( is => 'rw', isa => 'Vehicle::Properties' ); has appearance => ( is => 'rw', isa => 'Vehicle::Appearance' ); package Truck; use Moose; extends 'Vehicle'; # ... other methods package Car; use Moose; extends 'Vehicle'; # ... other methods
(And perhaps Truck::Performance and Car::Performance could inherit from Vehicle::Performance?)
Truck::Performance
Vehicle::Performance
However, this means the type constraints for both Car and Truck attributes will be in terms of Vehicle:: (e.g. Vehicle::Performance). That's OK as long as you don't try to put a Truck::Performance object in a Car::Performance accessor. It will be accepted because they are both Vehicle::Performance - but now you have a broken car. And what's the point of a type constraint if it doesn't stop you from doing this?
Vehicle::
It would be great to be able to do this:
package Vehicle use Moose; has performance => ( is => 'rw', isa => '(class of current object)::Performance' ); # ...
Then when you create objects which inherit from Vehicle, they automatically pick up the correct type constraint for performance.
performance
Note that doing this doesn't work:
package Vehicle use Moose; has performance => ( is => 'rw', isa => __PACKAGE__'::Performance' ); # ...
because this will give you a Vehicle::Performance type constraint every time, regardless of the actual class.
In fact there appears to be no way to do this in vanilla Moose. You're either stuck with compromising on your constraints, or writing out new accessors each time you create a new vehicle (which is not very DRY).
Enter MooseX::RelClassTypes - called as such because it allows attribute type constraints to be set relative to the current class (rather than the current package).
So now you can do:
package Vehicle; use Moose; with 'MooseX::RelClassTypes'; # include as a Moose Role has performance => ( is => 'rw', isa => '{CLASS}::Performance' ); has static_properties => ( is => 'rw', isa => '{CLASS}::Properties' ); has appearance => ( is => 'rw', isa => '{CLASS}::Appearance' );
So that:
# this works: my $car = Car->new( performance => Car::Performance->new; ) # but this errors Moosishly: my $car = Car->new( performance => Truck::Performance->new; );
which of course is what you really want.
Actually you probably don't need to modify the parameters - but nevertheless the following are provided:
parser
parser should be a CodeRef to a sub which will "parse" the isa (ie regex it and turn it into a real class name). The sub needs to have the following format:
sub { my ($isa_string, $parent_class) = @_; my $relative_class = ... ; # ( perform some kind of # operation to substitute # $parent_class in # $isa_string somehow) if ( $successful ){ # ie if substitution # actually occurred return $relative class; # this should end up as # a real class name } else { # it's important to # return undef if no return undef; # substitution happened # (meaning isa is not a # relative class name) to } # prevent unnecessary # processing }
where $isa_string will be the unparsed string (e.g. {CLASS}::Performance and $parent_class will be the class invoking the attribute (e.g. Car).
$isa_string
{CLASS}::Performance
$parent_class
If for some reason you don't like the default behaviour - which is to replace {CLASS} with the invoking class name - then you could use a custom parser routine to have the token to replace in a different format, e.g. (package). Or you could have your sub deduce the name from some whacky mathematical formula. To specify a custom parser routine, include it in your with... call:
{CLASS}
(package)
with...
package MyPackage; use Moose; with MooseX::RelClassTypes => { parser => sub { ... } };
(See MooseX::Role::Parameterized for more info on parameterized roles).
However, caution might be a good idea here. e.g. it is probably not a good idea to use a token which contains just text characters (and thus might coincide with part of a real module name) - this could lead to strange errors. Also remember that Moose has its own use for square brackets [] which is not a good idea to mess with.
auto_default
Since this module is intended for compound objects (ie objects that contain other objects in some kind of fixed heirarchy), it can be irritating to have to write
has performance => ( is => 'ro', isa => '{CLASS}::Performance', default => sub{ my $self = @_; my $package = ref( $self ).'::Performance'; return $package->new; } });
every time you just want the relevant class to be created via ->new. Therefore by default if MooseX::RelClassTypes sees this:
MooseX::RelClassTypes
has performance => ( is => 'ro', isa => '{CLASS}::Performance' );
it will automatically create an instance of the relevant class by calling ->new. (ie the above 2 code snippets are equivalent).
It will only do this if auto_default == 1 and if there is no default or builder specified in the attribute declaration.
auto_default == 1
default
builder
Note also that no attempt has been made to try to circumvent Moose's treatment of lazy attributes. So you can't do this:
lazy
has performance => ( is => 'ro', isa => '{CLASS}::Performance', lazy => 1 );
and expect it to work. Moose will error, complaining that a lazy attribute needs either a default or a builder. In summary, auto_default does not work with lazy attributes. You'll have to write out your default or builder sub in full if you want it to be lazy.
(I don't think this feature will ever be added. Firstly having lazy without an obvious default or builder looks confusing. Also lazy often means an attribute depends on another - which is not the case for a simple object instantiation using ->new.)
If you do not want MooseX::RelClassTypes to automatically create defaults in the manner described above, you should set auto_default to be 0:
package MyPackage; use Moose; with 'MooseX::RelClassTypes' => { auto_default => 0 };
MooseX::Role::Parameterized
Tom Gracey <tomgracey@gmail.com>
Copyright (C) 2018 by Tom Gracey
This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself.
To install MooseX::RelClassTypes, copy and paste the appropriate command in to your terminal.
cpanm
cpanm MooseX::RelClassTypes
CPAN shell
perl -MCPAN -e shell install MooseX::RelClassTypes
For more information on module installation, please visit the detailed CPAN module installation guide.