18

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.

Pat
  • 36,282
  • 18
  • 72
  • 87
Adam Libuša
  • 685
  • 7
  • 24

2 Answers2

19

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
      }
    )
  }
}
Brad Gilbert
  • 33,846
  • 11
  • 78
  • 129
  • Thank you for an exhausting answer. I accepted the other one, because it is shorter and might save some time to people who google. Basically, I wanted the last part of your answer -- the FETCH/STORE structure. Sorry if it was not clear from my question. – Adam Libuša Jul 29 '15 at 23:19
  • @AdamLibuša Often times with Perl 6 people assume a lot and ask the wrong question, so I wrote an answer for the question someone should generally have asked. Once I saw the other answer, I assumed that it would become the accepted one, which is fine. – Brad Gilbert Jul 30 '15 at 01:05
15

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.

darch
  • 4,200
  • 1
  • 20
  • 23
  • Thank you - this FETCH & STORE structure was what I was looking for. Kind of ugly though -- a lot of boilerplate. I was expecting something in the C# get/set blocks style. Without easily written custom accessor methods, the properties degenerate into public attributes. – Adam Libuša Jul 29 '15 at 23:17
  • @raiph I think I will edit the text of the question. I was really looking for a general purpose power tool (the title of the question suggests it, unlike the text of the question). I agree with you that in 90 % of the cases, `where` is the right tool. I did not want to resolve the problem presented, I wanted to know the tool and the example used was just an example. – Adam Libuša Aug 12 '15 at 15:03
  • @raiph There are some cases, where you want to do some dirty stuff, for example, log every getter usage during debugging, while ignoring the setter usages. It is good to know how. – Adam Libuša Aug 12 '15 at 15:13
  • It seems like `throws_like` is actually, or has since been renamed to, `throws-like`. – cat Feb 10 '16 at 17:02
  • According to this article, it actually seems to have been a deliberate decision, not to make it syntactically easy to create custom accessors in C# style. Too bad. https://6guts.wordpress.com/2016/11/25/perl-6-is-biased-towards-mutators-being-really-simple-thats-a-good-thing/ – Adam Libuša Dec 07 '16 at 20:12