Can anyone provide a code example how do you set watchers on variable change inside of class ? I tried to do it several ways using different features (Scalar::Watcher, trigger attribute of Moo) and OOP frameworks (Moo, Mojo::Base) and but all failed.
Below is my failed code for better understanding of my task. In this example i need to update attr2 everytime when attr1 changed.
Using Mojo::Base and Scalar::Watcher:
package Cat;
use Mojo::Base -base;
use Scalar::Watcher qw(when_modified);
use feature 'say';
has 'attr1' => 1;
has 'attr2' => 2;
has 'test' => sub { # "fake" attribute for getting access to $self
my $self = shift;
when_modified $self->attr1, sub { $self->attr2(3); say "meow" };
};
package main;
use Data::Dumper;
my $me = Cat->new;
$me->attr1;
warn Dumper $me;
say $me->attr1(3)->attr2; # attr2 is still 2, but must be 3
Using Moo and trigger:
package Cat;
use Moo;
use Scalar::Watcher qw(when_modified);
use feature 'say';
has 'attr1' => ( is => 'rw', default => 1, trigger => &update() );
has 'attr2' => ( is => 'rw', default => 1);
sub update {
my $self = shift;
when_modified $self->attr1, sub { $self->attr2(3); say "meow" }; # got error here: Can't call method "attr1" on an undefined value
};
package main;
use Data::Dumper;
my $me = Cat->new;
$me->attr1;
warn Dumper $me;
say $me->attr1(3)->attr2;
Any suggestion is much appreciated.
got error here: Can't call method "attr1" on an undefined value
This is because Moo expects a code reference as a trigger
for has
. You are passing the result of a call to update
. The &
here doesn't give you a reference, but instead tells Perl to ignore the prototypes of the update
function. You don't want that.
Instead, create a reference with \&foo
and do not add parenthesis ()
. You don't want to call the function, you want to reference it.
has 'attr1' => ( is => 'rw', default => 1, trigger => \&update );
Now once you've done that, you don't need the Scalar::Watcher any more. The trigger already does that. It gets called every time attr1
gets changed.
sub update {
my $self = shift;
$self->attr2(3);
say "meow";
};
If you run the whole thing now, it will work a little bit, but crash with this error:
Can't locate object method "attr2" via package "3" (perhaps you forgot to load "3"?) at
That's because attr1
returns the new value, and not a reference to $self
. All Moo/Moose accessors work like that. And 3
is not an object, so it doesn't have a method attr2
# this returns 1
# |
# V
say $me->attr1(3)->attr2;
Instead, do this as two calls.
$me->attr1(3);
say $me->attr2;
Here's a complete example.
package Cat;
use Moo;
use feature 'say';
has 'attr1' => ( is => 'rw', default => 1, trigger => \&update );
has 'attr2' => ( is => 'rw', default => 1 );
sub update {
my $self = shift;
$self->attr2(3);
say "meow";
}
package main;
my $me = Cat->new;
say $me->attr2;
$me->attr1(3);
say $me->attr2;
And the output:
1
meow
3
First of, Mojo::Base does not provide a trigger mechanism. But the way you implemented Scalar::Watcher could not work, because the test
method was never called. I tried hooking around new
in the Mojo::Base based class to do the when_modified
call in a place where it would always be called.
Everything from here is on is mere speculation.
The following snippet is what I tried, but it does not work. I'll explain why further below.
package Cat;
use Mojo::Base -base;
use Scalar::Watcher qw(when_modified);
use feature 'say';
has 'attr1' => '1';
has 'attr2' => 'original';
sub new {
my $class = shift;
my $self = $class->SUPER::new(@_);
when_modified $self->{attr1}, sub { $self->attr2('updated'); say "meow" };
return $self;
}
As you can see, this is now part of the new
call. The code does get executed. But it doesn't help.
The documentation of Scalar::Watcher states that the watcher should be there until the variable goes out of scope.
If when_modified is invoked at void context, the watcher will be active until the end of $variable's life; otherwise, it'll return a reference to a canceller, to cancel this watcher when the canceller is garbage collected.
But we don't actually have a scalar variable. If we try to do
when_modified $self->foo
then Perl does a method call of foo
on $self
and when_modified
will get that call's return value. I also tried reaching into the internals of the object above, but that didn't work either.
My XS is not strong enough to understand what is going on here, but I think it is having some trouble attaching that magic. It can't work with hash ref values. Probably that's why it's called Scalar::Watch.