Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Can the raku built in accessor methods be overridden by a child class?

Tags:

oop

raku

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!

like image 489
p6steve Avatar asked Sep 19 '20 18:09

p6steve


1 Answers

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.

like image 190
Brad Gilbert Avatar answered Nov 15 '22 11:11

Brad Gilbert