20

If you have an attribute that needs to be modified any time it is set, is there a slick way of doing this short of writing the accessor yourself and mucking around directly with the content of $self, as done in this example?

package Foo;
use Moose;

has 'bar' => (
    isa => 'Str',
    reader => 'get_bar',
);

sub set_bar {
    my ($self, $bar) = @_;
    $self->{bar} = "modified: $bar";
}

I considered trigger, but it seemed to require the same approach.

Is working directly with the hash reference in $self considered bad practice in Moose, or am I worrying about a non-issue?

FMc
  • 41,963
  • 13
  • 79
  • 132

4 Answers4

10

You can use the method modifier 'around'. Something like this:

has 'bar' => (
    isa    => 'Str',
    reader => 'get_bar',
    writer => 'set_bar'
);

around 'set_bar' => sub {
    my ($next, $self, $bar) = @_;
    $self->$next( "Modified: $bar" );
};

And yes, working directly with the hash values is considered bad practice.

Also, please don't assume that the option I presented is necessarily the right one. Using subtypes and coercion is going to be the right solution most of the time - if you think about your parameter in terms of a type that can possibly be reused throughout your application will lead to much better design that the kind of arbitrary modifications that can be done using 'around'. See answer from @daotoad.

aaa90210
  • 11,295
  • 13
  • 51
  • 88
  • 1
    For this to work you do need to declare a writer when defining the attribute. For eg, has 'bar' => (is=>'rw', isa=>'Str', writer=>'set_bar') – draegtun Sep 13 '09 at 15:22
  • re: hash values "bad practise": Its fine in triggers and examples i've seen (on Moose mailing list) do reaffirm this. – draegtun Sep 13 '09 at 15:26
  • draegtun: unless Moose has changed it the default writer is the attribute name. You can put around on that if you don't want to specify the writer. – Jeremy Wall Sep 14 '09 at 03:28
  • @Jeremy Wall: Without declaring a separate writer/reader then the "around" will affect the default getter as well as the default setter of an attribute so it will cause problems (throws an compilation error here for me). – draegtun Sep 14 '09 at 08:23
8

I think using the hash reference is fine within a trigger like this:

package Foo;
use Moose;

has 'bar' => ( 
    isa => 'Str', 
    is  => 'rw', 
    trigger => sub { $_[0]->{bar} = "modified: $_[1]" },
);

The trigger also fires when bar arg passed with the constructor. This won't happen if you define your own set_bar method or with a method modifier.

re: hash reference - Generally I think its best to stick with the attribute setters/getters unless (like with above trigger) there is no easy alternative.

BTW you may find this recent post about triggers by nothingmuch interesting.

AndyG
  • 39,700
  • 8
  • 109
  • 143
draegtun
  • 22,441
  • 5
  • 48
  • 71
  • Check out the Moose::Manual::Attributes on triggers - http://search.cpan.org/~drolsky/Moose-0.88/lib/Moose/Manual/Attributes.pod#Triggers – Drew Stephens Sep 12 '09 at 22:14
8

I'm not sure what kind of modification you need, but you might be able to achieve what you need by using type coercion:

package Foo;
use Moose;

use Moose::Util::TypeConstraints;

subtype 'ModStr' 
    => as 'Str'
    => where { /^modified: /};

coerce 'ModStr'
    => from 'Str'
    => via { "modified: $_" };

has 'bar' => ( 
    isa => 'ModStr', 
    is  => 'rw', 
    coerce => 1,
);

If you use this approach, not all values will be modified. Anything that passes validation as a ModStr will be used directly:

my $f = Foo->new();
$f->bar('modified: bar');  # Set without modification

This weakness could be OK or it could make this approach unusable. In the right circumstances, it might even be an advantage.

Alan Haggai Alavi
  • 72,802
  • 19
  • 102
  • 127
daotoad
  • 26,689
  • 7
  • 59
  • 100
3

If dealing with the hash directly is causing you concern, you could specify an alternate writer and then use that from within your own appropriately named 'public' writer.

package Foo;
use Moose;

has 'bar' => (
   isa => 'Str',
   reader => 'get_bar',
   writer => '_set_bar',
);

sub set_bar {
   my $self = shift;
   my @args = @_;
   # play with args;
   return $self->_set_bar(@args);
}

This, or triggers, would strike me as being a good approach depending on when and how you need to be manipulating the arguments.

(disclaimer: untested code written from memory, browsing SO on a netbook with flaky edge access)

jsoverson
  • 1,695
  • 1
  • 12
  • 17
  • This is a nice clean solution though it won't work with constructors. – mikegrb Sep 13 '09 at 16:00
  • @mikegrb: I'm not sure what exactly you want to do with constructors, but you can specify where a value is assigned via a constructor using the `init_arg` attribute modifier, and/or you could do some checks in the `BUILDARGS` method. – Ether Oct 27 '09 at 20:42