Writing to read-only attributes inside a Perl Moose class
Asked Answered
Q

3

6

Using Perl and Moose, object data can be accessed in 2 ways.

$self->{attribute} or $self->attribute()

Here is a simple example demonstrating both:

# Person.pm
package Person;

use strict;
use warnings;
use Moose;

has 'name' => (is => 'rw', isa => 'Str');
has 'age'  => (is => 'ro', isa => 'Int');

sub HAPPY_BIRTHDAY {
    my $self = shift;
    $self->{age}++;   # Age is accessed through method 1
}

sub HAPPY_BIRTHDAY2 {
    my $self = shift;
    my $age = $self->age();
    $self->age($age + 1);   # Age is accessed through method 2 (this will fail)
}

1;

# test.pl
#!/usr/bin/perl

use strict;
use warnings;
use Person;

my $person = Person->new(
    name => 'Joe',
    age  => 23,
);

print $person->age()."\n";

$person->HAPPY_BIRTHDAY();
print $person->age()."\n";

$person->HAPPY_BIRTHDAY2();
print $person->age()."\n";

I know that when you are outside of the Person.pm file it is better to use the $person->age() version since it prevents you from making dumb mistakes and will stop you from overwriting a read only value, but my question is...

Inside of Person.pm is it best to use $self->{age} or $self->age()? Is it considered bad practice to overwrite a read-only attribute within the module itself?

Should this attribute be changed to a read/write attribute if its value is ever expected to change, or is it considered acceptable to override the read-only aspect of the attribute by using $self->{age} within the HAPPY_BIRTHDAY function?

Quiff answered 14/9, 2015 at 19:26 Comment(0)
F
8

When using Moose, the best practice is to always use the generated accessor methods, even when inside the object's own class. Here are a few reasons:

  1. The accessor methods may be over-ridden by a child class that does something special. Calling $self->age() assures that the correct method will be called.

  2. There may be method modifiers, such as before or after, attached to the attribute. Accessing the hash value directly will skip these.

  3. There may be a predicate or clearer method attached to the attribute (e.g. has_age). Messing with the hash value directly will confuse them.

  4. Hash keys are subject to typos. If you accidentally say $self->{aeg} the bug will not be caught right away. But $self->aeg will die since the method does not exist.

  5. Consistency is good. There's no reason to use one style in one place and another style elsewhere. It makes the code easier to understand for newbs as well.

In the specific case of a read-only attribute, here are some strategies:

  1. Make your objects truly immutable. If you need to change a value, construct a new object which is a clone of the old one with the new value.

  2. Use a read-only attribute to store the real age, and specify a private writer method

For example:

package Person;
use Moose;

has age => ( is => 'ro', isa => 'Int', writer => '_set_age' );

sub HAPPY_BIRTHDAY {
    my $self = shift;
    $self->_set_age( $self->age + 1 );
}

Update

Here's an example of how you might use a lazy builder to set one attribute based on another.

package Person;
use Moose;

has age     => ( is => 'rw', isa => 'Int', lazy => 1, builder => '_build_age' );
has is_baby => ( is => 'rw', isa => 'Bool', required => 1 );

sub _build_age { 
    my $self = shift;
    return $self->is_baby ? 1 : 52
}

The lazy builder is not called until age is accessed, so you can be sure that is_baby will be there.

Setting the hash element directly will of course skip the builder method.

Filigreed answered 14/9, 2015 at 19:45 Comment(8)
Great answer @friedo. One question though, what do you do when you want to set an attributes value conditionally like so: $person->{age} = $person->is_baby() ? 1 : 52;. How do you do something like that following the correct Moose format?Quiff
@tjwrona1992 in that case, I would use a lazy builder method for the age attribute.Filigreed
Interesting, i will look into lazy builder methods.Quiff
@tjwrona1992 I added an example of how you might use a lazy builder.Filigreed
Thanks @friedo, that looks like what I need!Quiff
Your _real_age suggestion is a bad idea. Instead, use a read-only attribute with a private writer method: is => 'ro', writer => '_set_age'. That way, you don't incur the overhead of two method calls just to read the attribute.Oubliette
You can also introduce a trait to your age attribute. That will give you the birthday method and you can still make sure the age only goes up. We don't get younger after all. Let's use the Counter trait. has age => ( is => 'ro', traits => ['Counter'], isa => 'Num', handles => { celebrate_birthday => 'inc' }, ); Now we have a read-only age that has to be a number. Trying to set it will blow up, but you can increment by calling $bob->celebrate_birthday.Formulaic
Haven't messed with traits much, I will need to look into them! Thanks @FormulaicQuiff
S
4

I don't think $self->{age} is a documented interface, so it's not even guaranteed to work.

In this case I'd use a private writer as described in https://metacpan.org/pod/Moose::Manual::Attributes#Accessor-methods:

has 'weight' => (
    is     => 'ro',
    writer => '_set_weight',
);

You could even automate this using 'rwp' from https://metacpan.org/pod/MooseX::AttributeShortcuts#is-rwp:

use MooseX::AttributeShortcuts;

has 'weight' => (
    is => 'rwp',
);
Scamp answered 14/9, 2015 at 19:40 Comment(3)
$self->{age} will work since Moose objects are HashRefs but I do see your point. That looks like it is probably the "best practice" way of doing things.Quiff
@tjwrona1992 Sure, they're hashrefs, but is it documented that an attribute called age will be stored under the key age?Scamp
@Scamp it's somewhat implied, everyone knows it's true, and it couldn't change without breaking a significant amount of code... but no, I can't find any doc that clearly states it as a fact :) I consider it implicit in the fact that the default metaclass is also the minimal one on top of vanilla perl conventions, but I do recognize the fuzziness of that argument.Tiffin
B
1

Out-of-the-box perl isn't type safe and doesn't have much in the way of encapsulation, so it's easy to do reckless things. Moose imposes some civilization on your perl object, exchanging security and stability for some liberty. If Moose gets too stifling, the underlying Perl is still there so there are ways to work around any laws the iron fist of Moose tries to lay down.

Once you have wrapped your head around the fact that you have declared an attribute read-only, but you want to change it, even though you also said you wanted it to be read-only, and in most universes you declare something read only because you don't want to change it, then by all means go ahead and update $person->{age}. After all, you know what you are doing.

Brena answered 14/9, 2015 at 19:51 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.