Search code examples
perlmoose

Moose: change read-write attributes to read-only after constructing the object


I want to change attributes of an object to read-only after calling its BUILD method. How can I do this?

(Context: My program loads this object, which I want to be carved in stone after it has been created, from YAML using MooseX::YAML, and then changes its attributes in the BUILD method, to work around the limitation of YAML described here. More specifically, my YAML code declares a directory and a bunch of files under it, but there appears to be no way to express in YAML that all those files must be in that directory. I could, of course, prepend this directory name to all these filenames, making them absolute, but this would mean that 1) when I change my mind about the location of the directory, I have to change all entries for those files, instead of changing only the directory name, and 2) changing the directory name from inside the program before creating the object would be painful and error-prone.)

(added later:) A minimal working example.

yaml1:

# invalid YAML, unfortunately:
dir: &dir /here
file1: *dir/foo
file2: *dir/bar
# ... and a lot more

yaml2:

# works, but requires repeating '/here/':
dir: /here
file1: /here/foo
file2: /here/bar
# ...

yaml3:

# works, but requires making changes to attribute values:
dir: /here
file1: foo
file2: bar
# ...

program.perl:

use strict;
use warnings;
use lib '.';
use MooseX::YAML qw (LoadFile);
use Try::Tiny;

foreach (1..3) {
  my $datafile = "yaml$_";
  print STDERR "Loading $datafile...\n";
  try {
    LoadFile ("yaml$_");
  } catch {
    print STDERR "$_";
  };
}

Creating all these files in a directory and running "perl program.perl" from there, I get the following output:

Loading yaml1...
YAML::XS::Load Error: The problem:

    did not find expected alphabetic or numeric character

was found at document: 1, line: 2, column: 12
while scanning an alias at line: 2, column: 8
Loading yaml2...
Loading yaml3...

The file 'mypkg.pm' shows changes I have to make to attributes when using yaml3.

mypkg.pm:

package mypkg;
use Moose;
use File::Spec;

has 'dir' => (isa => 'Str', is => 'ro');
# have to allow modifying these values in BUILD
#                                              (!!)
has [qw (file1 file2)] => (isa => 'Str', is => 'rw');

sub BUILD {
  my ($self) = @_;
  $self->file1 (File::Spec->catfile ($self->dir, $self->file1));
  $self->file2 (File::Spec->catfile ($self->dir, $self->file2));
  # ... etc.
  # everything done, I would like all further changes to the attribute 
  # values to be impossible from now on
}

__PACKAGE__->meta->make_immutable;

Solution

  • Here is a way you can write to read-only attributes within the BUILD() sub:

    package mypkg;
    use Moose;
    use File::Spec;
    
    has [qw (file1 file2)] => (isa => 'Str', is => 'ro');
    has dir => (isa => 'Str', is => 'ro');
    
    sub BUILD {
        my ($self) = @_;
        # Note that we use $self->{file1} instead of $self->file1 in order to 
        # fool Moose and write to its read-only attributes
        $self->{file1} = File::Spec->catfile($self->dir, $self->file1);
        $self->{file2}  = File::Spec->catfile($self->dir, $self->file2);
    }
    

    See also Writing to read-only attributes inside a Perl Moose class