Why does `BUILD` not see attribute from parent class?

Viewed 139
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.

5 Answers

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' }
}

You may just want this code:

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

TL;DR All attributes are private to avoid the "fragile base class" problem. One option among many is to call the method you setup in A.

Why does BUILD not see attribute from parent class?

Quoting the doc section Attributes within the Objects page of the Language category:

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

This way Raku avoids the "fragile base class" problem. Quoting Wikipedia with my added emphasis:

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

Raku does this.¹

Call the method you setup in A

This code looks natural:

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

Quoting again from the doc 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 has done that -- you've written has $.name in A instead of just has $!name. So your code generates a .name method (public to any user of an instance of A, or a subclass, or class that mixes in A) as well as a $!name attribute (private to A).²

But you haven't used that accessor method. Here's one way to do that so your code works. It just needs 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")
  1. is rw makes the public .name accessor method a read/write one instead of the default read only one.

  2. 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.)

Footnotes

¹ Raku does more too. 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).

² Note that one can override the autogenerated method. At an extreme, using self.name in A or B might actually run a method in A that makes a cup of tea and returns "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

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!

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

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.

Related