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!
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With