Home > OS >  Moose - Retain the original value of an attribute in a second attribute
Moose - Retain the original value of an attribute in a second attribute

Time:07-20

I'm trying to create an object that will fetch a resource from the web, and needs to remember both where the resource was found eventually, as well as what the original URL was that we gave it.

I don't want to have to specify the URL twice, nor do I want to have loads of conditionals every time I want to use the URL to figure out whether I should use the "url" attribute or the "updated URL" attribute or some such, so I thought I'd create a second attribute with default property set to initialize from the original URL:

package foo;

use Moose;

has 'url' => (
    is => 'rw',
    isa => 'Str',
    required => 1,
);

has 'original_url' => (
    is => 'ro',
    isa => 'Str',
    default => sub { shift->url },
);

If this would have worked (see below), then I would have been able to change the url at will, and just use the url attribute whenever I need to access the "current" URL; and then later on, if I need to see if the url was ever changed, I can just compare the url attribute against the original_url attribute, and go from there.

The problem with this, however, is that the order of initialization of these attributes seems to be not well defined; sometimes the default sub for the original_url attribute is called before the url property has a value, which means it will return undef, which obviously causes an error.

I thought of making original_url a lazy attribute, add a predicate and then add a trigger on url to update original_url to the old value if its predicate says it hasn't been set yet, but it turns out that triggers are called after the value has been changed, so I don't think I can do that.

Is there a method to accomplish what I'm trying to do that won't cause errors?

CodePudding user response:

What about setting the value in the BUILD?

#!/usr/bin/perl
use warnings;
use strict;

{   package Foo;
    use Moose;

    has url => (
        is       => 'rw',
        isa      => 'Str',
        required => 1,
    );

    has original_url => (
        is       => 'ro',
        writer   => 'copy_url',
        isa      => 'Str',
        init_arg => undef,
    );

    sub BUILD {
        my ($self) = @_;
        $self->copy_url($self->url);
    }
}

my $o = 'Foo'->new(url => 'a');
$o->url('b');
$o->original_url eq 'a' or die;

You could do it in BUILDARGS, too, but I don't think it's the right place for such an action:

    has _original_url => (
        reader => 'original_url',
        isa    => 'Str',
    );

    around BUILDARGS => sub {
        my ($orig, $class, %args) = @_;
        $args{_original_url} = $args{url};
        $class->$orig(%args)
    }

CodePudding user response:

There's no need to use BUILD or BUILDARGS.

Just use init_arg to cause both attributes to be initialized from the same argument.

#!/usr/bin/perl
use v5.14;
use warnings;

{
   package Foo;

   use Moose;

   has url => (
      is       => 'rw',
      isa      => 'Str',
      required => 1,
   );

   has original_url => (
      is       => 'ro',
      init_arg => 'url',
   );
}

my $o = Foo->new( url => 'a' );
say $o->url;                      # a
say $o->original_url;             # a

This approach makes it trivial and efficient to "backup" multiple attributes.

for my $name (qw( url ... )) {
   has "original_$name" => (
      is       => 'ro',
      init_arg => $name,
   );
}

Whichever technique you use, you probably want to avoid isa on the attribute for the original. There's no point in validating the input twice, and I suspect it would lead to confusing messages if you did.

  • Related