Search code examples
perlmoose

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


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?


Solution

  • 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.