0

I'm writing a script to help me get proficient in Moose. I've got the following bit of code:

package Dir;
use Moose;
use Modern::Perl;
use File;

has 'dirs' =>             (is => 'ro', isa => 'HashRef[Dir]' );  
has 'files' =>            (is => 'ro', isa => 'HashRef[File]'); 
has 'dir_class' =>        (is => 'ro', isa => 'ClassName', default => 'Dir');
has 'file_class' =>       (is => 'ro', isa => 'ClassName', default => 'File');

sub BUILD {
  my $self = shift;
  my $path = $self->path;
  my $name = $self->name;
  my (%dirs, %files);

  # populate dirs attribute with LaborData::Data::Dir objects
  opendir my $dh, $path or die "Can't opendir '$path': $!";

  # Get files and dirs and separate them out
  my @dirs_and_files = grep { ! m{^\.$|^\.\.$} } readdir $dh;
  closedir $dh or die "Can't closedir '$path': $!";
  my @dir_names         = grep { -d "$path/$_" } grep { !m{^\.}  } @dirs_and_files;
  my @file_names        = grep { -f "$path/$_" } grep { !m{^\.}  } @dirs_and_files;

  # Create objects
  map { $dirs{$_}         = $self->dir_class->new  ( path => $path . '/' . $_ ) } @dir_names;
  map { $files{$_}        = $self->file_class->new ( path => $path . '/' . $_ ) } @file_names;

  # Set attributes
  $self->dirs         ( \%dirs );
  $self->files        ( \%files );
}

The code results in the following error: died: Moose::Exception::CannotAssignValueToReadOnlyAccessor (Cannot assign a value to a read-only accessor at reader Dir::dirs

To get around this error, I could either make the attributes rw or use builder methods for the dirs and files attributes. The former solution is undesirable and the latter solution will require duplication of code (for example, the directory will need to be opened twice) and so is also undesirable.

What is the best solution to this problem?

StevieD
  • 6,925
  • 2
  • 25
  • 45

2 Answers2

2

You can assign a writer to your read-only attribute and use that internally from your BUILD. Name it with an _ to indicate it's internal.

package Foo;
use Moose;

has bar => ( is => 'ro', writer => '_set_bar' );

sub BUILD {
    my $self = shift;

    $self->_set_bar('foobar');
}

package main;
Foo->new;

This will not throw an exception.

It's essentially the same as making it rw, but now the writer is not the same accessor as the reader. The _ indicates that it's internal, so it's less undesirable than just using rw. Remember that you cannot really protect anything in Perl anyway. If your user wants to get to the internals they will.

simbabque
  • 53,749
  • 8
  • 73
  • 136
  • OK, thanks so much. This seems like the best way to accomplish this. – StevieD Feb 04 '17 at 19:28
  • @simbabque: So every `ro` attribute *must* be given a `writer`, otherwise it will stay forever uninitialised? Also, please explain your joke above, as I appear to be exceptionally dumb tonight. – Borodin Feb 04 '17 at 21:21
  • @Borodin There used to be a `use StevesPerlTools;` line before the edit. – melpomene Feb 04 '17 at 21:26
  • 1
    @Borodin normally you initialize `ro` attributes through the constructor. `Foo->new( bar => 123 );` will set it. The point here is that some attributes get constructed during `BUILD`, which happens after the object was constructed (`BUILDARGS` is before). So if some of them are not passed in (either on purpose, by accident or because you removed the `init_arg`) and you still want to set them, you can add a `writer` for the attribute. And the `_` indicates that others should keep out. See the edit history of the question for the joke. :) – simbabque Feb 04 '17 at 21:30
  • 2
    @Borodin A `ro` attribute can be initialized through the constructor (`Foo->new(bar => 42)`), using a `default` value, or a `builder` method. If none of those apply, the attribute will [remain unset](https://metacpan.org/pod/Moose::Manual::Attributes#Predicate-and-clearer-methods). If there is no private `writer`, yes, it will stay that way forever. – melpomene Feb 04 '17 at 21:30
-2

I found one possible solution, though it's frowned upon:

  # Set attributes
  $self->{dirs}  =    \%dirs;
  $self->{files} =    \%files;
StevieD
  • 6,925
  • 2
  • 25
  • 45