views:

235

answers:

2

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?

+6  A: 

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 => ...,
);
Randal Schwartz
I'm not sure I follow. Could you write that out?
Schwern
edits have been made to answer. Sadly, you can't use the same formatting in a comment to a comment. That's silly.
Randal Schwartz
There is some highlighting in `comments`
Brad Gilbert
Text formatting in comments http://meta.stackoverflow.com/questions/2115/text-formatting-now-allowed-in-comments-list-of-proven-and-disproven-abilities
Brad Gilbert
+2  A: 

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?

jrockway
That exposes an internal performance detail to the user. To the user its a list. Your remark about implementing it elsewhere... hmm... maybe I can make a SearchList type? (This is also the first I've seen of Moose::Meta::Attribute::Native! Had to do some digging to figure out that's where 'set' and such were coming from)
Schwern
See edit. (and here are some extra characters for the validation routine...)
jrockway
That's getting somewhere, though $obj->allowed_values returns a hash ref not a list. And there's more than 3 values, that was just a quick example!
Schwern
It's worth noting that there is no "is => 'ro'" and hence no read accessor. The allowed_values method that's created just returns a list of the keys.
jrockway
Instead of implementing the hash/list caching in a separate class, do it in a modification of the meta attribute class (i.e. a trait) (which either accepts a new format type in `isa`, or perhaps a new attribute option that turns on the caching) and create a new subtype with array->hash coercion as you have described.
Ether