8

I'm trying to 'override' the built in accessors that raku generates for public $. attributes. However, I could use a bit of help trying to work out why this code fails...

class Measure {
    has $.value is rw
}

class Angle is Measure {
    multi method value( $x ) { 
        nextwith( $x % 360 );
    }   
}

my $m = Measure.new( value => 27 );
$m.value = 42;     
say $m;   #Measure.new(value => 42)

my $a = Angle.new( value => 27 );
$a.value = 43; 
say $a;   #Error...

Cannot resolve caller value(Angle:D: ); none of these signatures match:
    (Angle: $x, *%_)

Your guidance would be very helpful!

librasteve
  • 6,832
  • 8
  • 30
  • This question brings renewed attention to [parts of an answer I wrote to another of your questions](https://stackoverflow.com/a/59673538/1077672). Read either the whole answer, or focus on the part starting "The above isn't quite right though." and footnote 2. `Angle`'s `value` method is shadowing `Measure`'s but with the wrong signature; it has an argument (`$x`) but accessor methods must have zero arguments. Put it all together and you get the behavior we see here. – raiph Sep 19 '20 at 21:09
  • hi @raiph - thank you for the feedback - I guess it's rather non intuitive (for me) that $. accessors do not take an argument so I am condemned to going round the same small circle (well, maybe I'll learn this time) – librasteve Sep 20 '20 at 18:58

1 Answers1

8

When you declare an attribute as being public by using a ., Raku will create a method with the same name.

That method takes no arguments.
Even if it is rw.
(If you declare the attribute as is rw then the generated method is what is actually marked with is rw.)

When you use nextwith it dispatches to the method in the parent class.

In this case that method takes no arguments.


Here is something that at least works:

class Angle is Measure {
    method value( $x ) { 
        callwith() = $x % 360;
    }   
}

my $a = Angle.new( value => 27 );
#$a.value = 43; 
$a.value(43);

Of course that means that Angle.value isn't an lvalue like Measure.value is.
(lvalue means that it can be on the left side of =.)

So let's do that.

Since we need to do a calculation as part of calling .value we need to return a Proxy

class Angle is Measure {
    method value() is rw { 
        Proxy.new:
            FETCH => -> $ { self.Measure::value },
            STORE => -> $, $x {
                self.Measure::value = $x % 360;
            }
    }   
}

Note that we can't just use callsame or similar in those blocks because they start a new dispatch chain.
Instead we need to call the version of the method in the Measure class.

You can use callsame or similar if you bind the result of that call to a variable which you use as part of a closure.
(I used $attr because it is bound to the actual attribute scalar.)

class Angle is Measure {
    method value is rw {
        my $attr := callsame();
        Proxy.new:
            FETCH => -> $ { $attr },
            STORE => -> $, $x {
                $attr = $x % 360;
            }
    }
}

Personally I think that Measure should probably be a role, as that makes things easier as you can just access the attribute directly.

role Measure {
    has $.value is rw
}

class Angle does Measure {
    method value() {
        Proxy.new:
            FETCH => -> $ { $!value },
            STORE => -> $, $x {
                $!value = $x % 360;
            }
    }   
}

I also have a problem with an angle being declared as just a number without saying it is in Degrees instead of say Radians or Gradians.

Actually you don't even declare it as being a number.

So I might try something like this:

role Measure {
    has Real $.value is rw;
}

role Angle {…}

class Degrees  {…}
class Radians  {…}
class Gradians {…}

role Angle does Measure {
    method Degrees  ( --> Degrees  ) {…}
    method Radians  ( --> Radians  ) {…}
    method Gradians ( --> Gradians ) {…}
}

class Degrees does Angle {
    method value() {
        Proxy.new:
            FETCH => -> $ { $!value },
            STORE => -> $, $x {
                $!value = $x % 360;
            }
    }
    method Degrees  () { self }
    method Radians  () { !!! } # needs to actually be implemented here
    method Gradians () { !!! }
}
class Radians does Angle {
    method value() {
        Proxy.new:
            FETCH => -> $ { $!value },
            STORE => -> $, $x {
                $!value = $x % τ;
            }
    }
    method Degrees  () { !!! }
    method Radians  () { self }
    method Gradians () { !!! }
}
class Gradians does Angle {
    method value() {
        Proxy.new:
            FETCH => -> $ { $!value },
            STORE => -> $, $x {
                $!value = $x % 400;
            }
    }
    method Degrees  () { !!! }
    method Radians  () { !!! }
    method Gradians () { self }
}

Honestly I don't like that either because you are treating a value as a container.

Basically you have it working like this, where you can't have a constant Angle.

 class Foo {
     has Angle $.bar;
 }

 my $foo = Foo.new( bar => Degrees.new( value => 27 ) );
 $foo.bar.angle = 43;

I think that you should require it to work like this, where you can choose whether the Angle is constant.

 class Foo {
     has Angle $.bar is rw;
 }

 my $foo = Foo.new( bar => Degrees.new( value => 27 ) );
 $foo.bar .= new( value => 43 );

Doing it this way would make it so that you can just straight up delete the value methods in all of the subclasses, and replace them with a simple TWEAK. (Which is something that you really needed anyway.)
Of course you would also need to remove is rw from $.value.

I would make it so that you can call .new with just a single value rather than value => 27.

role Measure {
    has Real $.value;

    multi method new ( $value ) {
        samewith( :$value )
    }
}

role Angle {…}

class Degrees  {…}
class Radians  {…}
class Gradians {…}

role Angle does Measure {
    method Degrees  ( --> Degrees  ) {…}
    method Radians  ( --> Radians  ) {…}
    method Gradians ( --> Gradians ) {…}
}

class Degrees does Angle {
    submethod TWEAK { $!value %= 360 }
    method Degrees  () { self }
    method Radians  () { !!! } # needs to actually be implemented here
    method Gradians () { !!! }
}
class Radians does Angle {
    submethod TWEAK { $!value %= τ }
    method Degrees  () { !!! }
    method Radians  () { self }
    method Gradians () { !!! }
}
class Gradians does Angle {
    submethod TWEAK { $!value %= 400 }
    method Degrees  () { !!! }
    method Radians  () { !!! }
    method Gradians () { self }
}
 class Foo {
     has Angle $.bar is rw;
 }

 my $foo = Foo.new( bar => Degrees.new( 27 ) );
 $foo.bar = Degrees.new( 43 );

There is something that I want you to notice about that last version.
There is [almost] no code there.
It is mostly declarative code, which tends to be more noticeable when it is wrong.

(You would need to fill out those parts with !!!, but there shouldn't be very much code there.)


Anyway my point is that, yes you can do that [with Proxy], but there is a reason it is more difficult.
You are looking at the problem from a direction that to some extent goes against the design philosophies of Raku.

Brad Gilbert
  • 33,846
  • 11
  • 78
  • 129
  • Dear Brad - thanks for the comprehensive reply. I am not keen to use the Proxy method since the built in $. accessors are very, very close to my view of the most natural way to consume Units. Angle is a distinctive exception to most units - Length, Mass and so on naturally have Real values. The example I give was golfed to remove the Real type on value to highlight the specific frustration. My design (Physics::Measure) is a little different from yours - but TIMTOADY I guess! https://github.com/p6steve/raku-Physics-Measure – librasteve Sep 20 '20 at 19:08