http://qs321.pair.com?node_id=1211944

bobf has asked for the wisdom of the Perl Monks concerning the following question:

I've been learning Moose and I came across unexpected behavior: method modifiers (such as 'after') do not seem to work with attribute triggers. This is an issue because I want a subclass to extend a method that is defined as a trigger in the parent. The issue and one workaround is shown below.

use strict; use warnings; package Foo; use Moose; has 'attrib' => ( is => 'rw', trigger => \&attrib_changed ); has 'workaround' => ( is => 'rw', trigger => \&workaround_changed ); sub attrib_changed { print " in attrib_changed\n"; # called } after 'attrib_changed' => sub { print " in 'after' attrib_changed\n"; # not called }; sub workaround_changed { my ( $self ) = @_; print " in workaround_changed\n"; # called $self->not_a_trigger; } after 'workaround_changed' => sub { print " in 'after' workaround_changed\n"; # not called }; sub not_a_trigger { print " in not_a_trigger\n"; # called } after 'not_a_trigger' => sub { print " in 'after' not_a_trigger\n"; # called }; my $foo = Foo->new; print "Calling attrib( 1 ):\n"; $foo->attrib( 1 ); print "Calling workaround( 1 ):\n"; $foo->workaround( 1 );
Output: Calling attrib( 1 ): in attrib_changed Calling workaround( 1 ): in workaround_changed in not_a_trigger in 'after' not_a_trigger

I could not find anything in the Moose docs or online that documents this behavior as a known limitation. I'd be surprised if I were the first one to notice it, so it makes me wonder whether I am trying to do something that is best done some other way. Thoughts?

TIA

Update: Cross-posted to moose@perl.org

Replies are listed 'Best First'.
Re: Moose triggers & method modifiers
by choroba (Cardinal) on Mar 29, 2018 at 07:28 UTC
    This is the first time I see a modifier modifying a method defined in the same class. I've always used modifiers with inheritance or roles, modifying a parent or required method. Nevertheless, you can't wrap a trigger coming from a parent class, either.

    That's because the trigger is defined as a code reference, not as a sub name. It means you can modify e.g. a predicate, because it's defined by name, because modifiers redefine a method of the given name.

    I see two ways how to wrap a trigger in a more Moosey way: either define a new trigger in the overridden attribute, or define the trigger as a sub that calls a method that you later modify (predicate overriding shown in the example, too):

    #!/usr/bin/perl use warnings; use strict; use feature qw{ say }; { package My; use Moose; has a1 => ( is => 'rw', trigger => \&a1_changed, predicate => 'has_attr' ); has a2 => ( is => 'rw', trigger => sub { $_[0]->a2_changed(@_[ 1 .. $#_ ]) } ) +; sub a1_changed { print ref shift, ": a1 changed.\n"; } sub a2_changed { print ref shift, ": a2 changed.\n"; } __PACKAGE__->meta->make_immutable; no Moose; } { package My::Child; use Moose; extends 'My'; has '+a1' => ( trigger => \&a1_changed ); sub a1_changed { print "Trigger 1 wrapped.\n"; $_[0]->SUPER::a1_changed; }; around a2_changed => sub { print "Trigger 2 wrapped\n"; $_[0]->(@_[ 1 .. $#_ ]); }; around has_attr => sub { print "Predicate wrapped\n"; }; __PACKAGE__->meta->make_immutable; no Moose; } my $o = 'My'->new; my $ch = 'My::Child'->new; $o->a1(2); $ch->a1(2); say $o->has_attr; say $ch->has_attr; $o->a2(3); $ch->a2(3);

    ($q=q:Sq=~/;[c](.)(.)/;chr(-||-|5+lengthSq)`"S|oS2"`map{chr |+ord }map{substrSq`S_+|`|}3E|-|`7**2-3:)=~y+S|`+$1,++print+eval$q,q,a,

      Thanks very much for the excellent explanation and examples! Between the Moose IRC channel and my own experimentation I cobbled together similar solutions, but the one you provided is much more clear.

      In my real code, I use modifiers in child classes and roles (as you suggested), but when I was working on the example for this post I thought it might remove one more variable if I put everything in the same class. A weird design, I admit, but it demonstrated the issue.

      Initially, I implemented your first method (extending the attribute in the child class). It seemed to work ok, but this part of the docs (Moose::Manual::Attributes) made me worry about fragility and future breakage:

      Attribute Inheritance and Method Modifiers

      When an inherited attribute is defined, that creates an entirely new set of accessors for the attribute (reader, writer, predicate, etc.). This is necessary because these may be what was changed when inheriting the attribute.

      As a consequence, any method modifiers defined on the attribute's accessors in an ancestor class will effectively be ignored, because the new accessors live in the child class and do not see the modifiers from the parent class.

      In particular, I didn't want to remember which changes in the parent/child class would be ineffectual in a subset of classes that extend the attribute in question.

      As a result, I ended up using option 2. I like having "x_changed" as a named method, so I changed the trigger definition to be a coderef to an anonymous sub that calls "x_changed". Now I can safely modify that method without having to think too hard.

      Thanks again. ++