Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 634
Schwern Avatar asked Oct 11 '09 02:10

Schwern


2 Answers

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 => ...,
);
like image 93
Randal Schwartz Avatar answered Nov 17 '22 14:11

Randal Schwartz


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?

like image 33
jrockway Avatar answered Nov 17 '22 14:11

jrockway