9

Why can readonly array attributes be modified on a Raku class, but on the other hand, scalars cannot be modified?

How can I make @.baz "readonly"?

class Boo {
    has $.bar;
    has @.baz;
};

my $boo = Boo.new;

$boo.baz = (1, 2); # works ... ?
say $boo.baz;

$boo.bar = 1; #fails ... expected

My rakudo version:

This is Rakudo version 2020.05.1 built on MoarVM version 2020.05
implementing Raku 6.d.
Elizabeth Mattijsen
  • 25,654
  • 3
  • 75
  • 105
Julio
  • 5,208
  • 1
  • 13
  • 42
  • Just guessing... Is this because `$boo.baz = (1, 2);` is not really creating a new array and assigning its reference to `baz`? Perhaps this `$boo.baz = (1, 2);` is internally doing something like `remove_elements_from_baz; baz.push: 1; baz.push: 2` – Julio Sep 24 '20 at 08:13

2 Answers2

8

The baz attribute is read only.

The thing is that the baz attribute is an Array which has mutable elements.


When you call $boo.baz you basically get a reference to the array.

Once you have a reference to the array you can do anything that you could normally do to the array.

say $boo.baz.VAR.name; # @!baz

When you assign to an array, what you are really doing is telling the array that it will have new contents.

my @a = ('a','b','c');
say @a.WHICH; # Array|94070988080608

@a = (1, 2, 3);
say @a.WHICH; # Array|94070988080608

Notice that the .WHICH doesn't change. That is because it is still the same array. It just has new contents.

The exact same thing happens when you assign to a public array attribute.
You are not assigning a new array, you are altering the existing one.

All of the behaviours of a variable are not intrinsic to the variable, they are instead handled by an object.
In the case of arrays the object that handles the assignment is Array.


The simplest fix is to just overload the autogenerated accessor method.

class Boo {
    has $.bar;
    has @.baz;

    method baz () { @!baz.List }
}

If you never plan on changing the values in @!baz you could make it a List itself

class Boo {
    has $.bar;
    has @.baz is List;
}
Brad Gilbert
  • 33,846
  • 11
  • 78
  • 129
  • 3
    I think the confusion isn't why you can edit the elements — that's explained fairly easily by the object being internally mutable, but consider `class A {has @.a is List}; my $a = A.new(a=>(1,2)); $a.a = (3,4)` The reference isn't being changed, even though it looks like I'm attempting to assign a brand new list to it. Instead, a listy assignment is done, with an apparent attempt at keeping the original referent intact – user0721090601 Sep 24 '20 at 04:27
  • 1
    Yes, the `$boo.baz[0] = 9;` was expected. I Should have removed it from the question, sorry. I don't quite understand why can I assign a new list/array to the attribute – Julio Sep 24 '20 at 07:54
  • @Julio The answer is that you aren't assigning a new list or array. You are telling the existing array that it should have new contents. – Brad Gilbert Sep 25 '20 at 03:04
  • 1
    @Julio you can see it's readonly by trying to bind it to another value: `class Boo { has @.bar }; Boo.new.bar := (33) ` will yield `OUTPUT: «===SORRY!=== Error while compiling ␤Cannot use bind operator with this left-hand side␤` – jjmerelo Sep 25 '20 at 07:57
0

My vote goes to the 'is List' version suggested by @BradGilbert ... I tried it with defaults and got this Awesome Error message:

===SORRY!=== Error while compiling ...
Defaults on compound attribute types not yet implemented. Sorry.
Workaround: Create/Adapt TWEAK method in class Boo, e.g:

    method TWEAK() {
        @!baz := (initial values) unless @!baz;
    }
librasteve
  • 6,832
  • 8
  • 30