11
class A { has $.name; };
class B is A { submethod BUILD { $!name = 'foo' } };

This code looks natural but throws error.

Attribute $!name not declared in class B

Yes, it is not declared in class B, but we are in the partially constructed object during B::BUILD and documentation says that bless creates the new object, and then walks all subclasses in reverse method resolution order. So $!name attribute should be known for class B in this phase, right?

Is there any way to set parent class attributes during object construction without using new method? I know that new will do the trick here, but BUILD has a lot of syntactic sugar and BUILD / TWEAK feel more DWIMy and straightforward than resolving to low-level blessing in new.

Elizabeth Mattijsen
  • 25,654
  • 3
  • 75
  • 105
Pawel Pabian bbkr
  • 1,139
  • 5
  • 14

5 Answers5

5

Private attribute syntax ($!foo) is only available for attributes that are lexically visible. That's why they're private :-)

If class A would want other classes be able to change, it would need to provide a mutator method explicitely or implicitely (with is rw).

Or you could let class A trust class B as described at https://docs.raku.org/routine/trusts#(Type_system)_trait_trusts .

Still it feels you would do better using roles:

role A {
    has $.name is rw;
}
class B does A {
    submethod BUILD { $!name = 'foo' }
}
Elizabeth Mattijsen
  • 25,654
  • 3
  • 75
  • 105
  • `Private attributes are only lexically visible` - Well, `$.name` is not declared as private. That why I find this behavior confusing from users perspective. – Pawel Pabian bbkr Sep 05 '22 at 09:39
  • 1
    Roles are good solution but complicates everything if `A` can also be created as standalone instance. If `A` class needs `$.name` and `B` class needs to initialize `$.name` using role will not help. – Pawel Pabian bbkr Sep 05 '22 at 09:44
  • 1
    Roles autopun to classes when instantiated. So in that respect, you *can* still just say `A.new`: `role A { has $.foo }; dd A.new(foo => 42); # A.new(foo => 42)` – Elizabeth Mattijsen Sep 05 '22 at 10:17
  • Changed the first line to: "Private attribute syntax ($!foo) is only available for attributes that are lexically visible." to hopefully clarify the distinction. – Elizabeth Mattijsen Sep 05 '22 at 10:18
  • 1
    Oh, I completely forgot about Roles autopun. That is not the perfect solution - I still think Raku is less user-fiendly in that aspect than Perl + Moose + using `BUILDARGS`. However Role acting as standalone Class will do the trick in my case. So i'm flagging your answer as an solution. Thanks for your help! – Pawel Pabian bbkr Sep 05 '22 at 12:27
4

TL;DR All attributes are technically private. This design is a good one. You could just call a method in A from B. There are, of course, other options too.

Why doesn't BUILD see parent class attributes?

Quoting Wikipedia Fragile base class page problem:

One possible solution is to make instance variables private to their defining class and force subclasses to use accessors to modify superclass states.¹

Hence, per Raku Attributes doc:

In Raku, all attributes are private, which means they can be accessed directly only by the class instance itself.

B can call a method in A

This code looks natural:

class A { has $.name }
class B is A { submethod BUILD { $!name = 'foo' } }

Quoting again from Raku doc section linked above:

While there is no such thing as a public (or even protected) attribute, there is a way to have accessor methods generated automatically: replace the ! twigil with the . twigil (the . should remind you of a method call).

Your code generates a $!name attribute (private to A) plus a public .name method. Any code that uses the A class can call its public methods.

Your code hasn't used the autogenerated accessor method. But it could have done so with a couple small changes:

class A { has $.name is rw }                            # Add `is rw`
class B is A { submethod BUILD { self.name = 'foo' } }  # s/$!name/self.name/²
say B.new # B.new(name => "foo")

is rw makes the public .name accessor method a read/write one instead of the default read only one.

Not using is rw

As I now understand from your first comment below, an is rw accessor is disallowed given your requirements. You can achieve any effect that a class supports via its public interface.

Let's first consider a silly example so it's clear you can do anything that any methods can do. Using, say, self.name, in A or B, might actually run one or more methods in A that make a cup of tea and return 'oolong' rather than doing anything with A's $!name:

class A {
  has $.name = 'fred';     # Autogenerates a `method name` unless it's defined.
  method name { 'oolong' } # Defines a `method name` (so it isn't generated).
}
my \a = A.new;
say a;      # A.new(name => "fred")
say a.name; # oolong

Conversely, if an A object changes its $!name, doing so might have no effect whatsoever on the name of the next cup of tea:

class A {
  has $.name = 'fred';
  method name   { 'rooibos' }        # ignores `$!name`
  method rename { $!name = 'jane' }
}
my \a = A.new;
say a;      # A.new(name => "fred")
a.rename;
say a.name; # rooibos

To recap, you can (albeit indirectly) do anything with private state of a class that that class allows via its public API.


For your scenario, perhaps the following would work?:

class A {
  has $.name;
  multi method name { $!name }
  multi method name (\val) { once $!name = val }
}
class B is A {
  submethod BUILD { self.name: 42 }
}
my \a = B.new;
say a;       # B.new(name => 42)
say a.name;  # 42
a.name: 99;  # Does nothing
say a.name;  # 42

Footnotes

¹ Continuing to quote solutions listed by Wikipedia:

A language could also make it so that subclasses can control which inherited methods are exposed publicly.

Raku allows this.

Another alternative solution could be to have an interface instead of superclass.

Raku also supports this (via roles).

² self.name works where $!name does not. $.name throws a different compiler error with an LTA error message. See Using %.foo in places throws, but changing it to self.foo works.

raiph
  • 31,607
  • 3
  • 62
  • 111
  • Thanks for detailed explanation. In my case `rw` attributes workaround were no-go because of data security reasons. Accidentally changing them in code after object instances were created would cause fatal and costly data inconsistency. – Pawel Pabian bbkr Sep 06 '22 at 07:35
  • `have an interface instead of superclass` - Yes, I finally went with Roles as interface that can autopun as base class. Do not like it because it messes up natural inheritance and general code readability (causes similar issues as when artificial Roles must be introduced as workaround to have looped strict type checking). But it got the job done. – Pawel Pabian bbkr Sep 06 '22 at 07:38
4

The other option is to use the is built trait on attributes that you would like the default constructor to initialize.

Consider the following:

class A { 
  has $.name is built 
}

class B is A { }

B.new(name => "Foo").gist.say; # B.new(name => "Foo")

This allows descendend classes to use the named parameter matching the attribute in .new to initialize the value at object creation time. Please note that this will work whether the attribute is public "$." or private "$!".

Hope that helps!

Xliff
  • 194
  • 5
  • Unfortunately `built` param is not accessible in `BUILD` method in child class. Almost seems like a bug. The point of this trait is to allow build phase to manage parent class attributes. – Pawel Pabian bbkr Sep 07 '22 at 07:41
  • 1
    Yes, you can add a built param to BUILD, but there is a trick to it -- you have to specify it in the parameter list like this `submethod BUILD (:$!name) { }` – Xliff Sep 09 '22 at 13:12
1

Sorry that my answer is late in the day, but I feel that your original question is very well pitched and would like to add my variation.

class A { 
    has $!name;
   
    submethod BUILD( :$!name ) {}

    multi method name { $!name }
    multi method name(\v) { $!name := v }

    method gist(::T:) { "{::T.^name}.new( name => $!name )" }
}
class B is A { 
    submethod BUILD( :$name ) { self.name: $name // 'foo' }
}

say B.new;                 #B.new( name => foo )
say A.new(name => 'bar');  #A.new( name => bar )
say B.new(name => 'baz');  #B.new( name => baz )

Raku OO tries to do two mutually incompatible things:

  1. provide a deep OO (similar to C++ / Java)
  2. provide a lightweight OO (similar to Python / Ruby)

This is done by having a core that does #1 and then adding some sugar to it to do #2. The core gives you stuff like encapsulation, multiple inheritance, delegation, trust relationships, role based composition, delegation, MOP, etc. The sugar is all the boilerplate that Raku gives you when you write $. instead of $! so that you can just throw together classes to be lightweight datatypes for loosely structured data.

Many of the answers here bring suggestions from mode #2, but I think that your needs are slightly too specific for that and so my answer tilts towards mode #1.

Some notes to elaborate why I think this is a good solution:

  • you state that you cannot use is rw - this avoids traits
  • with proper method accessors, you have control over initialization
  • BUILD() is not constrained by the public accessor phasing
  • no need to go to roles here (that's orthogonal)

And some drawbacks:

  • you have to write your own accessors
  • you have to write your own .gist method [used by say()]

It is attributed to Larry that "everyone wants the colon(:)". Well, he had the last say, and that the Raku method call syntax self.name: 'foo' echos assignment self.name= 'foo' is, in my view, no accident and meant to ease the mental switch from mode #2 to #1. ;-)

Does Raku succeed to reconcile the irreconcilable? - I think so ... but it does still leave an awkward gear shift.

EDITED to add submethod BUILD to class A

librasteve
  • 6,832
  • 8
  • 30
  • Thanks. In your example `has $!name;` needs `is built` trait for `A` to be able to work as standalone class. And I have a feeling that `is built` would solve all those weird Raku-isms in class inheritance and initialization if only it would work in `BUILD` submethod. Current form seems to be half-baked - built trait that does not work when class is in BUILD phase :) – Pawel Pabian bbkr Sep 08 '22 at 20:36
  • Oh yes, thanks - good point! I have edited that aspect into my answer by way of a a submethod BUILD to class A rather than the more implicit trait since it aligns better with my general point. – librasteve Sep 08 '22 at 21:12
  • Accessors can also be written without the need for a multi by using the Proxy class. Given your above example, we can rewrite it like this: `method name is rw { Proxy.new(FETCH => -> $ { $!name }, STORE => -> $, \v { $!name = v } }` – Xliff Sep 09 '22 at 13:14
  • Hi @Xliff - my case is that mode #1 deep OO is often the best way to do OO in Raku when you want it done "right". To me, Proxy is a technique (like a more complicated version of ```$.``` public accessors) to sweeten OO so that you can assign an attr via a method with ```=``` Python style rather than doing it the formal way via a settor method with ```:```. My argument is that the settor (multi) method is much more in tune with pure OO principles and ```C.x: 42``` is not any harder than ```C.x=42```. – librasteve Sep 09 '22 at 21:14
1

Thanks everyone for great discussion and solution suggestions. Unfortunately there is no simple solution and it became obvious once I understood how Raku constructs object instances.

class A {
    has $.name is rw;
};
class B is A {
    submethod BUILD {
        self.A::name = 123;   # accessor method is already here
    }
};

B.new.name.say; # will print 123

So if inheritance is used Raku works from parent class to child class fully constructing each class along the way. A is constructed first, $.name param is initialized, public attribute accessor methods are installed. This A instance become available for B construction, but we are not in A build phase anymore. That initialization is finished. My code example shows what is happening with syntactic sugar removed.

The fact that

    submethod BUILD {
        self.name = 123;
    }

is available in class B during BUILD phase does not mean that we (as class B) have this attribute still available for construction. We are only calling write method on already constructed class A. So self.name = 123 really means self.A::name = 123.

TL;DR: Attributes are not collected from parent classes and presented to BUILD in child class to be set at the same time. Parent classes are constructed sequentially and only their method interfaces are available in child BUILD submethod.

Therefore

class A {
    has $.name;    # no rw
};
class B is A {
    submethod BUILD {
        $!name = 123;
    }
};

will not work because once we reach submethod BUILD in B class attribute $.name is already constructed and it is read only.

Solution for shallow inheritance:

Roles are the way to go.

role A {
    has $.name;
};
class B does A {
    submethod BUILD {
        $!name = 123;
    }
};

Roles are copied to class composing them, so class B sees this $.name param as their own and can initialize it. At the same time roles autopun to classes in Raku and standalone my $a = A.new( name => 123 ) can be used as a class.

However roles overdose can lead to orthogonal pattern issues.

Solution for deep inheritance:

There is none. You cannot have secure parent classes with read-only attribute behavior and initialize this attribute in child class builder, because at this moment parent class portion of self will be already constructed and attribute will be already read-only. Best you can do is to wrap attribute of parent class in private method (may be Proxy) and make it write-once this way.

Sad conclusion:

Raku needs improvement in this area. It is not convenient to use it for deep inheritance projects. Maybe new phaser is needed that will mash every attribute from parent classes in role-style and present them to BUILD at the same time. Or some auto-trust mechanism during BUILD. Or anything that will save user from introducing role inheritance and orthogonal role layout (this is doing stuff like class Cro::CompositeConnector does Cro::Connector when class Cro::Connector::Composite is Cro::Connector is really needed) to deep OO code because roles are not golden hammer that is suitable for every data domain.

Pawel Pabian bbkr
  • 1,139
  • 5
  • 14
  • hmmm - interesting points on role vs. class - but I don't buy your "sad conclusion" ... you already said the answer ```Best you can do is to wrap attribute of parent class in private method ``` – librasteve Sep 10 '22 at 19:01
  • Wrapping is not a complete solution. You will never be able to wrap for example `class A { has $.name is required }` to allow `$.name` to be set during child class construction. Correct me if I'm wrong but you cannot achieve read only required attribute in base class using methods. This is contradictory - if you use attribute methods then you already missed 'is required' train and best you can do is to defer Failure until name is called. – Pawel Pabian bbkr Sep 10 '22 at 21:10
  • ```class Person { has $.name is required }; class Person::Worker is Person { has $.access-card-id is required; submethod BUILD { $!access-card-id!) { $!name = security-info( $!access-card-id ) } };``` - This for example is very natural model which is hard to wrap if you do not want to loose strict `is required` constraint in base class. – Pawel Pabian bbkr Sep 10 '22 at 21:25
  • Good thing is that I'm finally learning to write clean Roles. Which follows SRP and do not have weird cross-dependencies. I got used to the pattern that every Role should be written to be able to autopun as independent class assuming its interface is implemented somewhere. Reading Raku / Cro and Red source was a small revelation for me. A lot of inheritance levels disappeared naturally in my code. – Pawel Pabian bbkr Sep 10 '22 at 21:40
  • I agree that Roles are / should be the first option for composition. That said, Attribute [wrappers](https://en.wikipedia.org/wiki/Encapsulation_(computer_programming)#Restricting_data_fields) are anyway the "deep" implementation in raku. When you use ```$.``` you are asking the raku compiler to apply some boilerplate wrappers. Raku attr traits (eg. ```is required```) are variants on the boilerplate (except ```is built```). I am sad that this boilerplate doesn't work smoothly with inheritance... but I do 'get' that any kind of sugar can only go so far before you need to roll up your sleeves! – librasteve Sep 11 '22 at 20:56