4

I have this code:

package Foo;
use Moo;

has attr => ( is => "rw", trigger => 1 );

sub _trigger_attr 
    { print "trigger! value:". shift->attr ."\n" }


package main;
use Foo;

my $foo = Foo->new( attr => 1 );
$foo->attr( 2 );

It returns:

$ perl test.pl
trigger! value:1
trigger! value:2

This is default, documented behavior of Triggers in Moo.

How can I disable trigger execution if the attribute is set via constructor?

Of course I can do it like this:

package Foo;
use Moo;

has attr        => ( is => "rw", trigger => 1 );
has useTriggers => ( is => "rw", default => 0 );

sub _trigger_attr 
{ 
    my $self = shift;
    print "trigger! value:". $self->attr ."\n" if $self->useTriggers 
}

package main;
use Foo;

my $foo = Foo->new( attr => 1 );
$foo->useTriggers( 1 );
$foo->attr( 2 );

And get:

$ perl testt.pl
trigger! value:2

So it works, but ... it feels wrong ;).

gib
  • 738
  • 1
  • 8
  • 16

3 Answers3

8

I don't know much about Moo, but in Moose you can implement your own code after the constructor. If you can do something like this in Moo it would give you the desired effect.

sub BUILD {
    my $self = shift;

    # Sets "useTriggers" AFTER the object is already constructed.
    $self->useTriggers(1);
};

This would cause useTriggers to be set just after construction so the trigger would be active after the object is constructed, but not before it is constructed.

So you should be able to write:

my $foo->new(attr => 1);
$foo->attr(2);

And get the same output.

tjwrona1992
  • 8,614
  • 8
  • 35
  • 98
  • In Moo I get: `Can't call method "useTriggers" on unblessed reference` – gib Feb 05 '18 at 15:29
  • 1
    hmmm, maybe try using the BUILD subroutine instead? get rid of `BUILDARGS` and try adding `sub BUILD { my $self = shift; $self->useTriggers(1); }`. Again, I'm not sure if this will act differently in `Moo` than it does in `Moose`. – tjwrona1992 Feb 05 '18 at 15:33
  • 1
    Try the new code in the edited answer and let me know if it works. :) – tjwrona1992 Feb 05 '18 at 15:36
7
package Foo;
use Moo;

has attr => ( accessor => '_attr' );

sub attr { 
    my $self = shift;
    my $rv = $self->_attr(@_);
    print "trigger! value: ", $rv if @_;
    return $rv;
}


package main;
use Foo;

my $foo = Foo->new( attr => 1 );
$foo->attr( 2 );
ikegami
  • 367,544
  • 15
  • 269
  • 518
  • If you have many such attributes, you can write a sub to generate the public accessor (and maybe even the attribute). – ikegami Feb 05 '18 at 14:53
0

Because Perl 5.36 was released with signatures no longer being experimental things got complicated in Moose ecosystem.

For Moo nothing changes, this useTriggers trick or building custom accessors stay as valid answers.

For Moose and Mouse when value was changed through attribute accessor old value was also passed. So trigger called from constructor and trigger called on existing attribute were distinguishable by arity. Common pattern was:

sub _change_attribute {
    my ( $self, $new ) = @_;

    return unless int @_ == 3;    # no old value, we're in constructor

    do something...

With signatures there is new issue - one cannot inspect @_ if signatures are used (this is consider experimental, throws a warning and is generally discouraged). So one may fall into this trap when adapting signature to variable trigger sub arity:

sub _change_attribute ( $self, $new, $old = undef ) {

    return unless defined $old;

    do something...
}

This will work only if attribute itself cannot be Undef. Otherwise trigger will not work as expected on

  • $object->attribute( undef );
  • $object->attribute( 123 ); # ooops !

change.

The hacky solution is to mistype old param on purpose as slurpy array:

sub _change_attribute ( $self, $new, @old ) {

    return unless @old;   # no old value, we're in constructor

    do something...
}

This will fire properly when value is changed through attribute accessor (does not matter if attribute can be Undef) and will fire and skip logic when initialized from constructor.

Pawel Pabian bbkr
  • 1,139
  • 5
  • 14