2

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.

Paul Serikov
  • 2,550
  • 2
  • 21
  • 36
  • The thing about Mojo::Base is that it isn't SUPPOSED to be a generic object system. It provides simple behavior and is very fast. We always support building your Mojolicious apps with Moo(se)? and when you want behavior like this, then that's the best solution! – Joel Berger Apr 11 '17 at 20:12

1 Answers1

5

The Moo part

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

Why Scalar::Watcher does not work with Mojo

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.

simbabque
  • 53,749
  • 8
  • 73
  • 136
  • great, it works, many thanks for detailed explanation! would be glad if someone will review Mojo::Base + Scalar::Watcher part :) Now I'm making some architecture decisions and would like to avoid using Moo or Moose for not to overweight app if it's possible. – Paul Serikov Apr 10 '17 at 15:29
  • I'm working on that. I don't think you can get that to work. Also, it's expensive. The docs of Scalar::Watcher even say you should only use it for debugging. Use Moo instead. It's very light-weight. Unless you run on a raspberry you should be fine. – simbabque Apr 10 '17 at 15:30
  • 1
    "*If you run this, it will work a little bit, but crash with this error:"* That part took me a while to understand because it looks like you're still talking about the `update` subroutine, whereas you're actually referring to the calling code in the `main` package of the OP's code. – Borodin Apr 10 '17 at 16:37
  • @Borodin I was in a hurry. I'll make it clearer later. Feel free to edit if you want. – simbabque Apr 10 '17 at 16:39