Rewriting object attributes - best way to do it using Moose? - perl

Let's see whether the SO question entry robot prediction, apparently issued based on just the question title, will come true:
The question you're asking appears subjective and is likely to be closed.
Using Perl/Moose, I'd like to bridge a mismatch between two ways merchant articles are represented. Let an article have name, quantity and price. The first way this is represented is with quantity set to any numeric value, including decimal values, so you can have 3.5 meters of rope or cable. The second one, which I have to interface with, is, alas, inflexible, and requires quantity to be an integer. Hence I have to rewrite my object to set quantity to 1 and include the actual quantity in the name. (Yes, this is a hack, but I wanted to keep the example simple.)
So the story here is that one property's value affects other properties' values.
Here's working code:
#!perl
package Article;
use Moose;
has name => is => 'rw', isa => 'Str', required => 1;
has quantity => is => 'rw', isa => 'Num', required => 1;
has price => is => 'rw', isa => 'Num', required => 1;
around BUILDARGS => sub {
my $orig = shift;
my $class = shift;
my %args = #_ == 1 ? %{$_[0]} : #_;
my $q = $args{quantity};
if ( $q != int $q ) {
$args{name} .= " ($q)";
$args{price} *= $q;
$args{quantity} = 1;
}
return $class->$orig( %args );
};
sub itemprice { $_[0]->quantity * $_[0]->price }
sub as_string {
return sprintf '%2u * %-40s (%7.2f) %8.2f', map $_[0]->$_,
qw/quantity name price itemprice/;
}
package main;
use Test::More;
my $table = Article->new({ name => 'Table', quantity => 1, price => 199 });
is $table->itemprice, 199, $table->as_string;
my $chairs = Article->new( name => 'Chair', quantity => 4, price => 45.50 );
is $chairs->itemprice, 182, $chairs->as_string;
my $rope = Article->new( name => 'Rope', quantity => 3.5, price => 2.80 );
is $rope->itemprice, 9.80, $rope->as_string;
is $rope->quantity, 1, 'quantity set to 1';
is $rope->name, 'Rope (3.5)', 'name includes original quantity';
done_testing;
I'm wondering, however, whether there's a better idiom to do this in Moose. But maybe my question is all subjective and deserves swift closing. :-)
UPDATE based on perigrin's answer
I've adapted perigrin's code sample (minor errors, and 5.10 syntax) and tagged my tests onto the end of it:
package Article::Interface;
use Moose::Role;
requires qw(name quantity price);
sub itemprice { $_[0]->quantity * $_[0]->price }
sub as_string {
return sprintf '%2u * %-40s (%7.2f) %8.2f', map $_[0]->$_,
qw/quantity name price itemprice/;
}
package Article::Types;
use Moose::Util::TypeConstraints;
class_type 'Article::Internal';
class_type 'Article::External';
coerce 'Article::External' =>
from 'Article::Internal' => via
{
Article::External->new(
name => sprintf( '%s (%s)', $_->name, $_->quantity ),
quantity => 1,
price => $_->quantity * $_->price
);
};
package Article::Internal;
use Moose;
use Moose::Util::TypeConstraints;
has name => isa => 'Str', is => 'rw', required => 1;
has quantity => isa => 'Num', is => 'rw', required => 1;
has price => isa => 'Num', is => 'rw', required => 1;
my $constraint = find_type_constraint('Article::External');
=useless for this case
# Moose::Manual::Construction - "You should never call $self->SUPER::BUILD,
# nor"should you ever apply a method modifier to BUILD."
sub BUILD {
my $self = shift;
my $q = $self->quantity;
# BUILD does not return the object to the caller,
# so it CANNOT BE USED to trigger the coercion.
return $q == int $q ? $self : $constraint->coerce( $self );
}
=cut
with qw(Article::Interface); # need to put this at the end
package Article::External;
use Moose;
has name => isa => 'Str', is => 'ro', required => 1;
has quantity => isa => 'Int', is => 'ro', required => 1;
has price => isa => 'Num', is => 'ro', required => 1;
sub itemprice { $_[0]->price } # override
with qw(Article::Interface); # need to put this at the end
package main;
use Test::More;
my $table = Article::Internal->new(
{ name => 'Table', quantity => 1, price => 199 });
is $table->itemprice, 199, $table->as_string;
is $table->quantity, 1;
is $table->name, 'Table';
my $chairs = Article::Internal->new(
name => 'Chair', quantity => 4, price => 45.50 );
is $chairs->itemprice, 182, $chairs->as_string;
is $chairs->quantity, 4;
is $chairs->name, 'Chair';
my $rope = Article::Internal->new(
name => 'Rope', quantity => 3.5, price => 2.80 );
# I can trigger the conversion manually.
$rope = $constraint->coerce( $rope );
# I'd like the conversion to be automatic, though.
# But I cannot use BUILD for doing that. - XXX
# Looks like I'd have to add a factory method that inspects the
# parameters and does the conversion if needed, and it is always
# needed when the `quantity` isn't an integer.
isa_ok $rope, 'Article::External';
is $rope->itemprice, 9.80, $rope->as_string;
is $rope->quantity, 1, 'quantity set to 1';
is $rope->name, 'Rope (3.5)', 'name includes original quantity';
done_testing;
I agree it provides a better separation of concerns. On the other hand, I'm not convinced this is a better solution for my purpose, as it adds complexity and does not provide for an automatic conversion (for which I would have to add more code).

Based on the information you provided in the comments, you're actually modeling two different but related things. You've encountered the ugliness of trying to keep these two things as a single Class. You end up not properly separating your concerns and have ugly dispatch logic.
You need to have two classes with a common API (a Role will enforce this) and a set of coercions to easily translate between the two.
First the API is really straight forward.
package Article::Interface {
use Moose::Role;
requires qw(name quantity price);
sub itemprice { $_[0]->quantity * $_[0]->price }
sub as_string {
    return sprintf '%2u * %-40s (%7.2f) %8.2f', map $_[0]->$_,
    qw/quantity name price itemprice/;
}
}
Then you have a Class to represent your internal Articles, again this is pretty trivial.
package Article::Internal {
use Moose;
has name => ( isa 'Str', is => 'rw', required => 1);
has [qw(quantity price)] => ( isa => 'Num', is => 'rw', required => 1);
# because of timing issues we need to put this at the end
with qw(Article::Interface);
}
Finally you have a class to represent your external articles. In this one you have to override some methods from the interface to deal with the fact that your attributes are going to be specialized[^1].
package Article::External {
use Moose;
has name => ( isa 'Str', is => 'ro', required => 1);
has quantity => ( isa => 'Int', is => 'ro', required => 1);
has price => (isa => 'Num', is => 'ro', required => 1);
sub itemprice { $_[0]->price }
# because of timing issues we need to put this at the end
with qw(Article::Interface);
}
Finally you define a simple coercion routine to translate between the two.
package Article::Types {
use Moose::Util::TypeConstraints;
class_type 'Article::Internal';
class_type 'Article::External';
coerce 'Article::Exteral' => from 'Article::Internal' => via {
Article::External->new(
name => $_->name,
quantity => int $_->quantity,
price => $_->quantity * $_->price
);
}
}
You can trigger this coercion manually with:
find_type_constraint('Article::External')->coerce($internal_article);
Additionally MooseX::Types can be used for this last part to provide cleaner sugar, but I chose to stick with pure Moose here.
[^1]: You may have noticed that I've made the attributes in the External article read-only. From what you've said these objects should be "consume only" but if you need the attributes to be writeable you'll need to define a coercion on quantity to deal with making sure that only Integers are stored. I'll leave that as an exercise to the reader.

Related

In Moose, how do I require one of multiple attributes?

I would like to be able to declare one of a set of mutually dependent attributes required.
Let's assume a simple example of Number 'nr_two' being 'nr_one' + 1, and 'nr_one' being 'nr_two' -1, with one of either having to be passed in upon initialization.
So far, I have seen this problem solved for example through BUILDARGS checks and a lazy builder on each:
has 'nr_one' => (
is => 'ro',
isa => 'Num',
lazy => 1,
builder => '_build_nr_one',
);
sub _build_nr_one { shift->nr_two - 1; }
has 'nr_two' => (
is => 'ro',
isa => 'Num',
lazy => 1,
builder => '_build_nr_two',
);
sub _build_nr_two { shift->nr_one + 1; }
around 'BUILDARGS' => sub {
my $orig = shift;
my $self = shift;
my $args = is_hashref($_[0])? $_[0] : { #_ };
die "Either nr_one or nr_two is required!" unless defined $args{nr_one} || defined $args{nr_two};
return $self->$orig($args);
};
Or, avoiding the around:
has 'nr_one' => (
is => 'ro',
isa => 'Num',
predicate => 'has_nr_one',
lazy => 1,
builder => '_build_nr_one',
);
sub _build_nr_one { shift->nr_two - 1; }
has 'nr_two' => (
is => 'ro',
isa => 'Num',
predicate => 'has_nr_two',
lazy => 1,
builder => '_build_nr_two',
);
sub _build_nr_two { shift->nr_one + 1; }
sub BUILD {
my $self = shift;
die "Either nr_one or nr_two is required!" unless $self->has_nr_one || $self->has_nr_two;
}
However, I am looking for something that can be declared on the attributes,
for example a grouping of some sort that can then be introspected and, for example, triggered in BUILD.
Ideally, I'd like to ship this into a generic role or Meta class to make it available
with some sort of nicer syntax, to avoid having to check for BUILD(ARGS) checks
or rely on the pod to declare things accurately.
Is there cpan module that could help with this, or a pattern someone is aware of to achieve this?
Any hints / partial solutions are appreciated, if not :)
An example of what I would imagine would look something like this:
has 'nr_one' => (
is => 'ro',
isa => 'Num',
lazy => 1,
builder => '_build_nr_one',
required_grouping => 'NumberGroup',
);
sub _build_nr_one { shift->nr_two - 1; }
has 'nr_two' => (
is => 'ro',
isa => 'Num',
lazy => 1,
builder => '_build_nr_two',
required_grouping => 'NumberGroup',
);
sub _build_nr_two { shift->nr_one + 1; }
# when initialized without any attributes, error thrown:
# "One of 'nr_one', 'nr_two' is required"
# or, probably easier: "NumberGroup required!"
I did not find a way to make a custom MooseX::Type or attribute trait automatically add a method modifier to BUILDARGS() that would validate the attributes. But it is simple to do that with a Moose::Role like this:
#! /usr/bin/env perl
package NumberGroup;
use Moose::Role;
around 'BUILDARGS' => sub {
my $orig = shift;
my $self = shift;
my $args = (ref $_[0]) eq "HASH" ? $_[0] : { #_ };
die "Either nr_one or nr_two is required!" unless defined $args->{nr_one} || defined $args->{nr_two};
return $self->$orig($args);
};
package Main;
use Moose;
with 'NumberGroup';
has 'nr_one' => (
is => 'ro',
isa => 'Num',
);
has 'nr_two' => (
is => 'ro',
isa => 'Num',
);
package main;
use strict;
use warnings;
Main->new();
Output:
Either nr_one or nr_two is required! at ./p.pl line 8.

Attribute is => 'Maybe[SomeSubtype]' returns Attribute () does not pass type constraint

I've created subtype Birth_d with coercion as shown below, and I'm trying to use it in combination with the built-in Maybe type, per Moose::Manual::Types.
I'm getting the error You cannot coerce an attribute (birth_d) unless its type (Maybe[Birth_d]) has a coercion. Here's complete test code:
package Student;
use Moose;
use Moose::Util::TypeConstraints;
use DateTime::Format::MySQL;
class_type 'Birth_d', { class => 'DateTime' };
coerce 'Birth_d',
from 'Str',
via { DateTime::Format::MySQL->parse_date( $_ ) };
has 'name' => (
isa => 'Str',
is => 'ro',
);
has 'birth_d' => (
isa => 'Maybe[Birth_d]', # This works: isa => 'Birth_d'
coerce => 1,
is => 'ro',
);
package main;
use Test::More;
my $student = Student->new(
name => 'Johnnie Appleseed',
birth_d => '2015-01-01'
);
is ( $student->birth_d->ymd(), '2015-01-01' );
my $student2 = Student->new(
name => 'Foo Bar',
birth_d => undef
);
is( $student2->birth_d, undef );
Replacing isa => 'Maybe[Birth_d]' with isa => 'Birth_d' works, but is not what is needed. I need to make the birth_d optional, and if not supplied, should be undef.
I should add, I tried using MooseX::Types to tuck this Birth_d type away in a separate place, but found its cavalier use of barewords a bit unorthodox, so I slowly backed away. I'm open to reconsidering it, if it makes sense to do so.
Moose does not do any chaining of coercions, in other words you have to tell it explicitly how to convert to a Maybe[Birth_d].
You can do this by reusing the existing coercion to Birth_d:
package Student;
use Moose;
use Moose::Util::TypeConstraints;
use DateTime::Format::MySQL;
# save the Moose::Meta::TypeConstraint object
# you can also get it with find_type_constraint('Birth_d')
my $birth_d = class_type 'Birth_d', { class => 'DateTime' };
coerce 'Birth_d',
from 'Str',
via { DateTime::Format::MySQL->parse_date( $_ ) };
subtype 'MaybeBirth_d',
as 'Maybe[Birth_d]';
coerce 'Maybe[Birth_d]',
from 'Str|Undef',
via { $birth_d->coerce($_) };
has 'name' => (
isa => 'Str',
is => 'ro',
);
has 'birth_d' => (
isa => 'Maybe[Birth_d]',
coerce => 1,
is => 'ro',
predicate => 'has_birth_d', # as per your comment
);
package main;
use Test::More;
my $student = Student->new(
name => 'Johnnie Appleseed',
birth_d => '2015-01-01'
);
is ( $student->birth_d->ymd(), '2015-01-01' );
my $student2 = Student->new(
name => 'Foo Bar',
birth_d => undef
);
is( $student2->birth_d, undef );
ok( $student2->has_birth_d );
done_testing;
I would find it more useful to not have a Maybe[Birth_d] type, but simply declare the attribute with the Birth_d type, and no "required" set.
That way, if a valid String is passed in, it will be accepted, an invalid String will lead to an error, and nothing just does not need to be passed in.
However, you can coerce to a maybe type:
subtype 'MaybeBirth_d',
as maybe_type(class_type('DateTime'));
coerce 'MaybeBirth_d',
from 'Str',
via { DateTime::Format::MySQL->parse_date( $_ ) };
has 'birth_d' => (
isa => 'MaybeBirth_d',
coerce => 1,
is => 'ro',
);
I just do not see the value of being able to pass in undef for a birthdate - how is that better than not setting it?
I would also like to suggest using no Moose::Util::TypeConstraints; and no Moose; at the end of your package, or namespace::autoclean; at the beginning, as well as __PACKAGE__->meta->make_immutable; at the end of your Student class.

Creating attribute defaults by calling a wrapped object

I have WrapperClass object that has an InnerClass object as an attribute. The InnerClass object has a weight attribute. My WrapperClass object also has a weight attribute and I want its default value to be whatever the value of the InnerClass object's weight attribute is.
#!/usr/bin/perl
package InnerClass;
use Moose;
has 'weight' => (
is => 'rw',
);
package WrapperClass;
use Moose;
has 'wrapped' => (
is => 'rw',
lazy => 1,
default => sub {InnerClass->new(weight => 1)},
);
has 'weight' => (
is => 'rw',
default => sub {
my $self = shift;
$self->wrapped->weight()
},
lazy => 1,
);
The code above works, but in reality InnerClass has many attributes which WrapperClass needs to do the same thing for. Ideally I would do something like this when I'm writing WrapperClass:
use Moose;
has 'wrapped' => (
is => 'rw',
);
my #getDefaultsFromWrappers
= qw(weight height mass x y z label); # etc ...
foreach my $attr (#getDefaultsFromWrappers) {
has $attr => (
is => 'rw',
default => sub {
# Somehow tell the default which attribute
# it needs to call from wrapped object?
my $self = shift;
$self->wrapped->???()
},
lazy => 1,
);
}
However, there is no way of passing an argument to a default or builder to tell it which attribute it is building. I've considered using caller but this seems like a hack.
Does anyone know how I could accomplish this style of attribute declaration or is it a case of declaring each attribute and its default separately?
You can use $attr where your question marks are because it is still in scope when you declare the attributes.
foreach my $attr (#getDefaultsFromWrappers) {
has $attr => (
is => 'rw',
default => sub { shift->wrapped->$attr() },
lazy => 1,
);
}
The following is a possible alternative, which you might want to use if your attribute declarations are not uniform:
has weight => (
is => 'rw',
isa => 'Num',
default => _build_default_sub('weight'),
lazy => 1,
);
has label => (
is => 'rw',
isa => 'Str',
default => _build_default_sub('label'),
lazy => 1,
);
sub _build_default_sub {
my ($attr) = #_;
return sub { shift->wrapped->$attr };
}
This may be better handled by method delegation and default values in the inner object.
With these, the example you gave can be better written as:
#!/usr/bin/perl
use strict;
use warnings;
package InnerClass;
use Moose;
has weight => (
is => 'rw',
default => 1,
);
package WrapperClass;
use Moose;
has wrapped => (
is => 'rw',
isa => 'InnerClass',
lazy => 1,
default => sub { InnerClass->new },
handles => [ 'weight' ],
);
package main;
my $foo = WrapperClass->new;
print $foo->weight;
Any additional defaults would be added as default on the InnerClass, and within the WrapperClass, add to wrapped 'handles' array ref to indicate that it should be delegated to that object.
If don't want the defaults to be applied to all instances of InnerClass, then you can remove the default from there, specify all attributes required (to give better error detection), and specify all attributes in the default constructor.

Why isn't Moose Role exclude excluding particular role attributes?

I have a Moose::Role that has (among other things):
package My::Role;
use strict;
use warnings;
use Moose::Role;
use MooseX::ClassAttribute;
class_has table => (
is => 'ro'
isa => 'Str',
lazy => 1,
);
has id => (
is => 'ro',
isa => 'Int',
predicate => 'has_id',
writer => '_id',
required => 0,
);
has other => (
is => 'rw',
isa => 'Int',
);
...
1;
Then, in a module that consumes that Role,
package Some::Module;
with 'My::Role' => {
-excludes => [qw( id table )]
};
has module_id => (
is => 'ro',
isa => 'Int',
);
...
1;
Then, in a script I'm instantiating an instance of Some::Module:
my $some_module = Some::Module->new({ other => 3 });
and I'm able to call
$some_module->id; # I'd expect this to die but returns undef.
However, I'm unable to call
$some_module->table; # this dies as I'd expect
As I'd expect calling $some_module->table causes the script to cease. Calling
$some_module->id doesn't.
When I use Data::Dumper to dump out the attribute list of the $some_module meta
class it show that the id attribute is defined but the table attribute is not.
Does anyone know why the 'id' attribute defined in the Role would not be excluded
from the meta class but the 'table' class_attribute would? The problem being, as
described above, is that users of Some::Module can call id() when they should be
required to call module_id().
Furthermore, when dumping $some_module object, the 'id' doesn't show up in the dump.
Edit:
Here's a sample that illustrates the problem. I've defined a role
that implements an id then I'm consuming the role in the package My::Product.
I'm excluding the id when consuming it however. When I print the attribute
from the meta object it shows that it is in fact there. I was under the impression
that excluding the id from a role when consuming it wouldn't allow it to be called.
I'd expect that it would not only be NOT in the meta object but also to die on
an attempt to call it.
#!/usr/bin/perl
package My::Model;
use Moose::Role;
use MooseX::ClassAttribute;
class_has first_name => (
is => 'rw',
isa => 'Str',
);
class_has last_name => (
is => 'rw',
isa => 'Str',
);
has id => (
is => 'rw',
isa => 'Int',
predicate => 'has_id',
writer => '_id',
required => 0,
);
1;
package My::Product;
use Moose;
use Class::MOP::Class;
use Data::Dumper;
with 'My::Model' => { -excludes => [ qw( first_name id ) ], };
has count => (
is => 'rw',
isa => 'Int',
);
has product_id => (
is => 'ro',
isa => 'Int',
required => 0,
predicate => 'has_product_id'
);
sub create_classes {
my #list = ();
foreach my $subclass (qw( one two three )) {
Class::MOP::Class->create(
"My::Product::"
. $subclass => (
superclasses => ["My::Product"],
)
);
push #list, "My::Product::$subclass";
}
return \#list;
}
__PACKAGE__->meta()->make_immutable;
1;
package main;
use strict;
use warnings;
use Data::Dumper;
my $product = My::Product->new();
my $classes = $product->create_classes();
my #class_list;
foreach my $class ( #{ $classes } ) {
my $temp = $class->new( { count => time } );
$temp->first_name('Don');
$temp->last_name('MouseCop');
push #class_list, $temp;
}
warn "what is the id for the first obj => " . $class_list[0]->id ;
warn "what is the first_name for the first obj => " . $class_list[0]->first_name ;
warn "what is the last_name for the first obj => " . $class_list[0]->last_name ;
warn "\nAttribute list:\n";
foreach my $attr ( $class_list[2]->meta->get_all_attributes ) {
warn "name => " . $attr->name;
# warn Dumper( $attr );
}
Edit 2:
Upon dumping the $attr I am seeing that first_name and id are in the method_exclusions.
'role_applications' => [
bless( {
'class' => $VAR1->{'associated_class'},
'role' => $VAR1->{'associated_class'}{'roles'}[0],
'method_aliases' => {},
'method_exclusions' => [
'first_name',
'id'
]
}, 'Moose::Meta::Class::__ANON__::SERIAL::8' )
]
I have no idea how the innards of this works but I believe this is to do with the fact that the two methods you are excluding are attribute methods. The only relevant article I can find is here, where it says:
A roles attributes are similar to those of a class, except
that they are not actually applied. This means that methods that are
generated by an attributes accessor will not be generated in the role,
but only created once the role is applied to a class.
Therefore I'm guessing the problem is that when your classes are being constructed, the role is applied (and the methods are excluded), but after that the role's attributes are applied and the accessor methods (including id and first_name) are constructed.
To demonstrate, change the id attribute to _id, give it a different writer and create an id sub to access it:
# This replaces id
has _id => (
is => 'rw',
isa => 'Int',
writer => 'set_id',
required => 0,
);
sub id {
my $self = shift;
return $self->_id();
}
The script will now die with an exception:
Can't locate object method "id" via package "My::Product::one" at ./module.pm line 89.

How do I best make triggered accessors with defaults in Moose?

I have a situation where I'd like to cache some calculations for use
later. Let's say I have a list of allowed values. Since I'm going to
be checking to see if anything is in that list I'm going to want it as
a hash for efficiency and convenience. Otherwise I'd have to grep.
If I'm using Moose it would be nice if the cache was recalculated each
time the list of allowed values is changed. I can do that with a
trigger easy enough...
has allowed_values => (
is => 'rw',
isa => 'ArrayRef',
trigger => sub {
my %hash = map { $_ => 1 } #{$_[1]};
$_[0]->allowed_values_cache(\%hash);
}
);
has allowed_values_cache => (
is => 'rw',
isa => 'HashRef',
);
And the two will stay in sync...
$obj->allowed_values([qw(up down left right)]);
print keys %{ $obj->allowed_values_cache }; # up down left right
Now let's say I want a default for allowed_values, simple enough
change...
has allowed_values => (
is => 'rw',
isa => 'ArrayRef',
trigger => sub {
my %hash = map { $_ => 1 } #{$_[1]};
$_[0]->allowed_values_cache(\%hash);
},
default => sub {
return [qw(this that whatever)]
},
);
...except setting the default doesn't call the trigger. To get it to
DWIM I need to duplicate the caching.
has allowed_values => (
is => 'rw',
isa => 'ArrayRef',
trigger => sub {
$_[0]->cache_allowed_values($_[1]);
},
default => sub {
my $default = [qw(this that whatever)];
$_[0]->cache_allowed_values($default);
return $default;
},
);
sub cache_allowed_values {
my $self = shift;
my $values = shift;
my %hash = map { $_ => 1 } #$values;
$self->allowed_values_cache(\%hash);
return;
}
The Moose docs are explicit about trigger not getting called when
the default is set, but it gets in the way. I don't like the
duplication there.
Is there a better way to do it?
I was recently faced with this, and after asking on the #moose channel, was told to handle it this way:
Mark cache_allowed_values as a lazy_build, have _build_cache_allowed_values reference the current allowed_values, and put a write-trigger on allowed_values that clears cache_allowed_values.
That way, no matter what order the values are asked for or saved, they'll always be right with the least amount of work.
Example:
has cache_allowed_values => (is => 'ro', lazy_build => 1);
sub _build_cache_allowed_values {
return { map { $_ => 1 } #{shift->allowed_values} };
}
has allowed_values => (
is => 'rw',
trigger => sub { shift->clear_cache_allowed_values },
default => ...,
);
I think you really want allowed_values to be a separate data structure with the efficiency and ordering properties you desire. Since it doesn't look like you care about the ordering, why not:
has 'allowed_values' => (
traits => ['Hash'],
isa => HashRef[Bool],
default => sub { +{} },
handles => {
_add_allowed_value => 'set',
remove_allowed_value => 'delete',
value_is_allowed => 'exists',
allowed_values => 'keys',
},
);
method add_allowed_value(Str $value){
$self->_add_allowed_value( $value, 1 );
}
In general, anything not specific to the class being implemented should probably be implemented elsewhere. Making arrays have faster lookup times is not really the job of whatever class you are writing, so it should be implemented elsewhere, and this class should use that class. (In the simple case, like the hash above, maybe it's OK to ignore this rule. But if it were any more complicated, you would definitely want to factor it out.)
Edit:
If you want the user to think this is a list, how about:
use MooseX::Types::Moose qw(Bool ArrayRef HashRef);
use MooseX::Types -declare => ['ListHash'];
subtype ListHash, as HashRef[Bool];
coerce ListHash, from ArrayRef, via { +{ map { $_ => 1 } #$_ } };
has 'allowed_values' => (
# <same as above>
isa => ListHash,
writer => 'set_allowed_values',
coerce => 1,
);
Now you can set allowed_values like:
my $instance = Class->new( allowed_values => [qw/foo bar/] );
$instance->set_allowed_values([qw/foo bar baz/]);
And access them like:
my #allowed_values = $instance->allowed_values;
... if $instance->value_is_allowed('foo');
And modify them:
$instance->remove_allowed_value('foo');
$instance->add_allowed_value('gorch');
This hides any underlying implementation details from the user.
BTW, is building the hash actually and using it significantly faster than a linear scan over 3 elements?