Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Using automatically generated methods for public attributes vs creating methods for accessing private attributes

Let's say I have a class with several attributes but I only need some of them for object construction; the remaining attributes's values depend on these public attributes. However, I'd like to still access the remaining attributes's values with methods named after the attributes. From the top of my head, there are two options:

First option:

  • Overload the new method. Or leave the new method as is, and instead use a BUILD submethod to both set the passed argument to the right attribute and set remaining attributes using this argument's value.
class Foo {
    has Int $.a;
    has Int $.b;
    has Int $.c;

    submethod BUILD(:$!a) {
        $!b = $!a ** 2;
        $!c = $!a ** 3;
    }
}
say Foo.new(:2a, :1b);   #=> Foo.new(a => 2, b => 4, c => 8)
say Foo.new(:2a, :1b).b; #=> 8

Second option:

Declare with $. only those attributes needed for object construction and modify the remaining attributes (declared with $!) with a TWEAK submethod after object construction. However, now I need to create accesor methods for these attributes.

class Bar {
    has Int $.a;
    has Int $!b;
    has Int $!c;

    submethod TWEAK {
        $!b = $!a ** 2;
        $!c = $!a ** 3;
    }

    method b { $!b }
    method c { $!c }
}
say Bar.new(:2a, :1b);   #=> Bar.new(a => 2)
say Bar.new(:2a, :1b).b; #=> 8

Questions

What's the best option? Are there any other options? What are the advantages/drawbacks for each option?

like image 707
Luis F. Uceta Avatar asked Jan 02 '20 18:01

Luis F. Uceta


Video Answer


2 Answers

Personally I'd go nice and simple :

class Foo { 
    has Int $.a; 
    has Int $!b = $!a ** 2; 
    has Int $!c = $!a ** 3;
    method b { $!b }
    method c { $!c }
}
say Foo.new(:2a, :1b); #=>  Foo.new(a => 2)
say Foo.new(:2a, :1b).b; #=> 4

Just use the default constructor and default values for the attributes and add a couple of basic read methods. The default constructor only updates public attributes so if you try and override b or c this is ignored.

If you wanted you could add a BUILD submethod to error if someone tries and sets them.

A good point raised in the comments for this case (and possibly the final use case) doing it like this :

class Foo { 
    has Int $.a; 
    method b() is pure { $!a ** 2 }
    method c() is pure { $!a ** 3 }
}
say Foo.new(:2a, :1b); #=>  Foo.new(a => 2)
say Foo.new(:2a, :1b).b; #=> 4

Depending on the complexity of the calculation you may want to use the is cached trait too.

like image 51
Scimon Proctor Avatar answered Sep 16 '22 22:09

Scimon Proctor


I read an article a couple of weeks ago that talked about this ambiguity between the constructor arguments of a class and its public interface but I cannot find it anymore.

But I am thinking you could leverage FALLBACK.

class Bar {
    has Int $.a;
    has Int $!b;
    has Int $!c;

    submethod TWEAK {
        $!b = $!a ** 2;
        $!c = $!a ** 3;
    }

    method FALLBACK( $name ) {
        self.^attributes.first( *.name eq "\$!$name" ).?get_value(self);
    }
}

say Bar.new(:2a);
say Bar.new(:2a).c;

That's kinda hacky though, and costly since the attribute lookups have to go though FALLBACK and introspection. What would be nice to have is a trait that would create an accessor but have no effect on the constructor. Something like

class Bar {
    has Int $.a;
    has Int $.b is shady; # or whatever name
    has Int $.c is shady;
}

But I am not aware such a thing exists.

like image 24
Holli Avatar answered Sep 20 '22 22:09

Holli