Skip to content

Instantly share code, notes, and snippets.

@j1n3l0
Created March 17, 2021 11:10
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save j1n3l0/349cb42fd3329fc6e672b4251b41cb7e to your computer and use it in GitHub Desktop.
Save j1n3l0/349cb42fd3329fc6e672b4251b41cb7e to your computer and use it in GitHub Desktop.
Trying to avoid mutating a readonly attribute
use Test2::V0 -target => 'Client';
subtest 'A class with a [readonly, class_type("URI")] attribute' => sub {
my $uri = 'https://example.com';
my $object = $CLASS->new( service_uri => $uri );
is(
$object,
object { call service_uri => $uri },
'should return the attribute unchanged',
);
$object->get('path');
is(
$object,
object { call service_uri => $uri },
'should not update the attribute at a distance',
);
};
done_testing();
package Client;
use Moo;
use Type::Utils qw< class_type >;
use Types::Standard -types;
use URI;
use experimental qw< signatures >;
has service_uri => (
coerce => 1,
is => 'ro',
isa => class_type('URI')->plus_constructors(Str, 'new'),
);
sub get ( $self, $path ) { $self->service_uri->clone->path_segments($path) }
1;
@j1n3l0
Copy link
Author

j1n3l0 commented Mar 17, 2021

In this instance you would have to clone the object otherwise the attribute will get mutated. Other ways around it would be to assign the attribute to a variable and use that.

@j1n3l0
Copy link
Author

j1n3l0 commented Mar 23, 2021

Interestingly, the Object::Pad solution has the same limitations:

use Object::Pad 0.36;

package Client;
class Client;

use Type::Utils qw< class_type >;
use Types::Standard -types;
use URI;

my $Uri = class_type('URI')->plus_constructors(Str, 'new');

has $service_uri :reader;

BUILD (%args) { $service_uri = $Uri->coerce( $args{service_uri} ) };

method get($path) { $service_uri->clone->path_segments($path) }

1;

@j1n3l0
Copy link
Author

j1n3l0 commented Mar 24, 2021

You could use the around method modifier to ensure that you are only ever passed a clone on the original object:

package Client;

use Moo;
use Type::Utils qw< class_type >;
use Types::Standard -types;
use URI;

use experimental qw< signatures >;

has service_uri => (
    coerce => 1,
    is     => 'ro',
    isa    => class_type('URI')->plus_constructors(Str, 'new'),
);

around service_uri => sub ( $orig, $self ) {
    $self->$orig()->clone();
};

sub get ( $self, $path ) {
    $self->service_uri->path_segments($path);
}

1;

The same solution exists for Object::Pad:

use Object::Pad 0.36;

package Client;
class Client;

use Class::Method::Modifiers qw< around >;
use Type::Utils qw< class_type >;
use Types::Standard -types;
use URI;

my $Uri = class_type('URI')->plus_constructors(Str, 'new');

has $service_uri :reader;

BUILD (%args) {
    $service_uri = $Uri->coerce( $args{service_uri} );
};

around service_uri => sub ( $orig, $self ) {
    $self->$orig()->clone();
};

method get($path) {
    $self->service_uri->path_segments($path);
}

1;

But the correct way to solve this with Object::Pad (as of v0.51) is:

use Object::Pad 0.51;

package Client;
class Client;

use Type::Utils qw< class_type >;
use Types::Standard -types;
use URI;

my $Uri = class_type('URI')->plus_constructors(Str, 'new');

has $service_uri :param;

ADJUST { $service_uri = $Uri->assert_coerce($service_uri) };

method service_uri () { $service_uri->clone() }

method get ($path) { $self->service_uri->path_segments($path) }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment