How to initialize attributes inherited in a sub-class?

Viewed 140

This code works as expected:

use v6.d;

class Foo {
    has $.name;

    submethod BUILD (:$!name = 'John') {};
}
my $f = Foo.new;
say $f;
# OUTPUT: Foo.new(name => "John")

However, when I add:

class Bar is Foo {
    submethod BUILD (:$!name = 'Jane') {};
}
my $b = Bar.new;
say $b;

I get this error message:

===SORRY!=== Error while compiling scratch.raku
Attribute $!name not declared in class Bar
at scratch.raku:14

How can I assign default values at construction time?

4 Answers

Here's an approach using TWEAK adapted from brian d foy's book, "Learning Raku" (formerly "Learning Perl6"), Chapter 12: "Classes":

use v6.d;

class Foo {
    has $!default-name = 'John';
    has $.custom-name  is rw;

    submethod TWEAK (:$custom-name) {
      self.custom-name = $custom-name // $!default-name;
      };
}

my $f = Foo.new;
say $f; 
put "This is class Foo with {$f.custom-name} as name.";


class Bar is Foo {}

my $b = Bar.new;
$b.custom-name = 'Jane';
say $b;
put "This is class Bar with {$b.custom-name} as name.";

Output:

Foo.new(custom-name => "John")
This is class Foo with John as name.
Bar.new(custom-name => "Jane")
This is class Bar with Jane as name.

Above class Foo's "John" takes the default-name when no custom-name is assigned. In the second example class Bar's "Jane" takes the custom name assigned to it.

You can modify class Bar to have its own default-name, and its own TWEAK submethod. Then the only difference between the original class Foo and the inherited class Bar is Foo {} appears to be declaring $.default-name as a public method.

https://www.learningraku.com/2018/08/14/table-of-contents/
https://www.oreilly.com/library/view/learning-perl-6/9781491977675/

Here is an edited version based on your comment. This way the default for the child class is set in the child class, but may be explicitly set if needed.

use v6.d;

class Foo {
    has $.name = 'John'
}

class Bar is Foo {
    method new( *%h ) { 
        %h<name> //= 'Jane';
        nextwith( |%h )
    }   
}

Foo.new.say;                     # OUTPUT: Foo.new(name => "John")
Bar.new.say;                     # OUTPUT: Bar.new(name => "Jane")
Bar.new(name => 'Dora').say;     # OUTPUT: Bar.new(name => "Dora")

Since my previous version relied on TWEAK, I thought it would be fun to try that way too.

class Foo {
    has $!name;

    multi method name {              #getter
        $!name 
    }   
    multi method name( $value ) {    #setter
        $!name = $value 
    }   

    submethod TWEAK {
        $!name //= 'John'
    }   

    method gist(::T:) {              #captures type of its invocant
        "{::T.^name}.new(name => \"$!name\")"
    }   
}

class Bar is Foo {
    submethod TWEAK( :$name ) { 
        self.name: $name // 'Jane'
    }   
}

Foo.new.say;                     # OUTPUT: Foo.new(name => "John")
Bar.new.say;                     # OUTPUT: Bar.new(name => "Jane")
Bar.new(name => 'Dora').say;     # OUTPUT: Bar.new(name => "Dora")

This is a bit trickier since the public attribute shorthand has $.name; auto-generates (i) public accessor getter/setter methods with (ii) a proxy for easy assignment and (iii) adjusts the .gist for a quick and easy view of all public attrs via .say. BUT these features are not ready until construction is done.

Therefore this example has to have explicit setter/getter multi methods with has $!name; as private attribute.

The public attribute shorthand is like training wheels for OO that provides easy use of basic OO as transparent data structures with a low learning curve (like in Python). Sidestepping this is like putting raku into more formal OO mode with proper encapsulation. As you climb the OO hill, with inheritance, roles, delegation, trust and so on, raku gently encourages you to get more formal.

Here we need to do this to get a way to access the $!name attribute in the parent class during construction.

It's easy enough now for Foo's TWEAK submethod to apply the default by assigning to it's private $!name.

Now Bar's TWEAK submethod can use the getter/setter since all regular methods are available at TWEAK time. (Actually you get the same outcome if you use BUILD instead of TWEAK in this example.)

A couple of other adjustments -

  • say calls .gist on an object - so we need to explicitly put private attributes back in with a custom .gist if you want to show them
  • here I use a ::T type capture so that I only have to do that once and the same parent method works for all child classes
  • need to use self. instead of $. at TWEAK time (all that $. virtual method machinery comes in afterwards), and
  • need to use the method syntax for setter methods self.name: 'value' (which IMO is a nicer way to go self.name('value')) since the public attr proxy is not there

The following is a bit out of context now after this edit, but I will leave here anyway.

The magic is that special methods new, BUILD and TWEAK will auto set named attrs based on their signature alone.

Here's why I prefer to use TWEAK instead of BUILD:

BUILD may set an attribute, but it does not have access to the contents of the attribute declared as its default as they are only applied later. TWEAK on the other hand is called after default values have been applied and will thus find the attributes initialized. So it can be used to check things or modify attributes after object construction.

I think this is close to what you want to do, though it requires making the field name mutable, here it's declared as "is rw":

class Foo {
    has $.name  is rw = 'John';  # easy way to specify a default
}
class Bar is Foo {
    submethod TWEAK (:$name) {
        self.name = $name // 'Jane';
    };
}

my $f = Foo.new;
say $f;   # Foo.new(name => "John")
my $b = Bar.new;
say $b;  # Bar.new(name => "Jane")

So that gives you a sub-class with a different default, though you might regard it as a drawback that you can change the name after object creation:

$b.name = 'Janezilla'; 
say $b;  # Bar.new(name => "Janezilla")

Yet another alternative - one that doesn't require making the property in Foo rw - is to use composition as opposed to inheritance combined with the handles trait.

use v6.d;

class Foo {
    has $.name = 'John' ;
}

class Bar {
    has Foo $!foo handles 'name' ;

    submethod TWEAK( :$name = 'Jane' ) {
        $!foo .= new: :$name
    }
}

say Foo.new.name ;                    # "John"
say Bar.new.name ;                    # "Jane"
say Bar.new(name => "Terrie").name    # "Terrie"

Arguably, the handles clause declares your intent more clearly - to adopt the 'name' property from Foo as your own. Some would say that composition is a better way to go than inheritance in any case - but obviously that's going to depend on the wider context you're doing this in.

Related