Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How does one write custom accessor methods in Perl6?

Tags:

raku

How does one write custom accessor methods in Perl6?

If I have this class:

class Wizard {
    has Int $.mana is rw;
}

I can do this:

my Wizard $gandalf .= new;
$gandalf.mana = 150;

Let's say I want to add a little check to a setter in my Perl6 class without giving up the $gandalf.mana = 150; notation (in other words, I don't want to write this: $gandalf.setMana(150);). The program should die, if it tries to set a negative mana. How do I do this? The Perl6 documentation just mentions it is possible to write custom accessors, but does not say how.

like image 701
Adam Libuša Avatar asked Jul 28 '15 00:07

Adam Libuša


2 Answers

With more recent versions of Rakudo there is a subset named UInt that restricts it to positive values.

class Wizard {
  has UInt $.mana is rw;
}

So that you're not stuck in a lurch if you need to something like this; here is how that is defined:
( you can leave off the my, but I wanted to show you the actual line from the Rakudo source )

my subset UInt of Int where * >= 0;

You could also do this:

class Wizard {
  has Int $.mana is rw where * >= 0;
}

I would like to point out that the * >= 0 in the where constraint is just a short way to create a Callable.

You could have any of the following as a where constraint:

... where &subroutine # a subroutine that returns a true value for positive values
... where { $_ >= 0 }
... where -> $a { $a >= 0 }
... where { $^a >= 0 }
... where $_ >= 0 # statements also work ( 「$_」 is set to the value it's testing )

( If you wanted it to just not be zero you could also use ... where &prefix:<?> which is probably better spelled as ... where ?* or ... where * !== 0 )


If you feel like being annoying to people using your code you could also do this.

class Wizard {
  has UInt $.mana is rw where Bool.pick; # accepts changes randomly
}

If you want to make sure the value "makes sense" when looking at all of the values in the class in aggregate, you will have to go to a lot more work.
( It may require a lot more knowledge of the implementation as well )

class Wizard {
  has Int $.mana; # use . instead of ! for better `.perl` representation

  # overwrite the method the attribute declaration added
  method mana () is rw {
    Proxy.new(
      FETCH => -> $ { $!mana },
      STORE => -> $, Int $new {
        die 'invalid mana' unless $new >= 0; # placeholder for a better error
        $!mana = $new
      }
    )
  }
}
like image 143
Brad Gilbert Avatar answered Sep 30 '22 18:09

Brad Gilbert


You can get the same accessor interface that saying $.mana provides by declaring a method is rw. Then you can wrap a proxy around the underlying attribute like so:

#!/usr/bin/env perl6
use v6;

use Test;
plan 2;

class Wizard {
    has Int $!mana;

    method mana() is rw {
        return Proxy.new:
            FETCH => sub ($) { return $!mana },
            STORE => sub ($, $mana) {
                die "It's over 9000!" if ($mana // 0) > 9000;
                $!mana = $mana;
            }
    }
}

my Wizard $gandalf .= new;
$gandalf.mana = 150;
ok $gandalf.mana == 150, 'Updating mana works';
throws_like sub {
    $gandalf.mana = 9001;
}, X::AdHoc, 'Too much mana is too much';

Proxy is basically a way to intercept read and write calls to storage and do something other than the default behavior. As their capitalization suggests, FETCH and STORE are called automatically by Perl to resolve expressions like $gandalf.mana = $gandalf.mana + 5.

There's a fuller discussion, including whether you should even attempt this, at PerlMonks. I would recommend against the above -- and public rw attributes in general. It's more a display of what it is possible to express in the language than a useful tool.

like image 23
darch Avatar answered Sep 30 '22 20:09

darch