tags:

views:

146

answers:

4

In our classes we have a pattern where we create an attribute to represent a calculated value. For obvious reasons we want to cache the calculated value and then invalidate the cache when one of the underlying values change.

So we currently have this:

package FooBar;
use Moose;

has 'foo' => (
        accessor => {
            'foo' => sub {
                my $self = shift;
                if (@_ > 0) {
                    # writer
                    $self->{foo} = $_[0];

      # reset fields that are dependant on me
      $self->{bar} = undef;
                }
                # reader part;
                return $self->{foo};
            }
        }
    );

has 'bar' => (
        accessor => {
            'bar' => sub {
                my $self = shift;
                if (@_ > 0) {
                    # writer
                    $self->{bar} = $_[0];
                }
                # reader part;
                $self->{bar} = calculate_bar($self->foo, $self->baz) 
                        if (not defined($self->{bar}));
                return $self->{bar};
            }
        }
    );

sub calculate_bar { ... }

This long hand method is getting very tedious and error prone when calculated values depend on other calculated values.

Is there a smarter/simpler way for 'bar' to monitor the attributes it depends on vs having 'foo' know who is dependent on it? Also how can I avoid setting bar via hash member access?

+9  A: 

If I understand you correctly, you can use triggers to clear attributes when one is set. Here's an example:

has 'foo' => (
    is      => 'rw',
    trigger => sub{
        my ($self) = @_;
        $self->clear_bar;
    }
);

has 'bar' => (
    is      => 'rw',
    clearer => 'clear_bar',
    lazy    => 1,
    default => sub{
        my ($self) = @_;
        return calculate_bar( ... );
    }
);

So, any writes to foo via $obj->foo($newvalue) will cause bar to be cleared, and recreated on next access.

Dan
I voted up, because this is the Moose way that requires writing the least voodoo but I think my solution is massively better. I also wanted to point out the obvious down side to this, your workload goes up with each attribute change because each attribute change must explicitly clear the lazy (calculated) attribute. If he has 5 attributes, and he changes each one 5 times before he calls the lazy attribute that is 24 wasted calls to clear the lazy (calculated) attribute. This is also abusing lazy to get get the benefit of memoization.
Evan Carroll
It's also backwards from a maintainability perspective; bar-depends-on-foo is logically a property of bar, not foo
ysth
This is a Moose-y way and a great answer but I was happiest to cut out any extra moose based code that just got in the way of the business logic.Thank you Dan.
clscott
A: 

Would this work?

#!/usr/bin/perl

package Test;

use Modern::Perl;
use Moose;

has a => (is => 'rw', isa => 'Str', trigger => \&change_a);
has b => (is => 'rw', isa => 'Str', trigger => \&change_b);
has c => (is => 'rw', isa => 'Str');

sub change_a
{
    my $self = shift;
    say 'update b';
    $self->b($self->a . ', bar');
}   

sub change_b
{
    my $self = shift;
    say 'update c';
}   

package main;

my $test = Test->new->a('Foo');

Output:

$ perl test.pl
update b
update c
zoul
I'm not sure why you think he wants to set b, from a. but this is a disaster of circular triggers in the making. Because you're setting it through the moose-provided public interfaces rather than meta an accidental change in b through the setter will cause its trigger to set to a which will cause its trigger... Setting attributes through triggers using the public interface is a bad idea.
Evan Carroll
Setting `$b` from `$a` was a way of saying that he can update the calculated value (`$b`) when one of the master values (`$a`) changes. And I don’t think there will be a trigger cycle if he simply wants to update the calculated properties. It might be that I simply don’t get your argument – have an example?
zoul
(But certainly this solution is worse than those above beause it does not recalculate `$b` lazily.)
zoul
+4  A: 

I think it is quite possible that you're making this harder on yourself by using an Attributes implicit memoization with lazy, when you could just make the memoization explicit making your whole program more transparent

has [qw/foo bar baz/] => ( isa => 'Value', is => 'rw' );

use Memoize;
memoize('_memoize_this');

sub old_lazy_attr {
    my $self = shift;
    _memoize_this( $self->attr1, $self->attr2, $self->attr3 );
}

sub _memoize_this {
    my @args = @_;
    # complex stuff
    return $result
}

See cpan's Memoize for information and control of the internal cache, also remember that a Memoized function can not be dependent on the state of the object. So the arguments must be passed in explicitly.

Evan Carroll
Hm. I have problems getting my head around using Memoize to cache object data. What happens if every instance of this class has different values? Memoize will cache them forever regardless of the fact they're no longer useful when the object is destroyed, right? Which means in a persistent app (and that's really the only sensible place to use Moose) you're potentially going to grow a huge, useless cache. No?Of course, you can mess about expiring stuff manually (I think!), but that's way more complexity over the Moose/lazy example above, for little gain..
Dan
I disagree fundamentally, not just is it /less/ complex and more transparent but the speed hit and gain is predictable and the logic is where it should be rather than hacked into other accessors. All you need to do is subclass Memoize::Expire and set the STORE sub to clear the cache, prior to writting to the hash.
Evan Carroll
I chose this as the answer as it drastically simplifies the code, which was what I was really striving for. The fact that the result of the calculation is not stored in the object itself it not a problem for my current implementation.Thanks EvanCaroll.
clscott
A: 

I haven't done any poking around in Moose internals and the meta object protocol, but I think this is a good time to do it.

You want to patch the code generation so that when you specify an attribute as

has 'foo' => ();
has 'bar' => ( 
    depends_on => [qw( foo )],
    lazy => \&calculate_bar,
);

the code generation phase creates code for the foo and bar attributes as you specified above.

How to do this is an exercise left to the reader. If I had a clue, I'd try to give you a start. Unfortunately, all I can advise you with is "This is a job for the MOP".

daotoad
This is way more work than the answer I accepted. It also seems impractical as it requires the testing and maintenance of a MooseX:: module, some type of plugin or a patch against Moose itself that will never be accepted into core.
clscott
Check out the `Extending` and `Meta` sections in the cookbook. It seems scary at first. When you read the docs then it doesn't look so bad. No matter how you solve your problem, the main thing is to minimize the fiddly code you have to maintain. If a meta approach does this, then good. Otherwise, use something else.
daotoad