I am fairly new to Moose, and have been trying to follow the best practices. I understand that we should try to make classes immutable when we can. My question is, after object construction, can we change the value of a public attribute using a setter and still have the class be immutable?
Yes. If a class is immutable, this only means we cannot add new attributes or new methods to the class. This allows the Moose system do do some neat optimizations.
Any instances of the class can still be mutated. In order for the instances to be immutable as well, all attributes must be readonly (is => 'ro').
Example:
package MyClass;
use Moose;
has attr => (is => 'rw'); # this attribute is read-write
__PACKAGE__->meta->make_immutable;
# after this, no new attributes can be added
Then:
my $instance = MyClass->new(attr => "foo");
say $instance->attr; # foo
$instance->attr("bar"); # here, we change the instance, but not the class
say $instance->attr; # bar
Related
I'm tinkering with Moose as introduced in Intermediate Perl. I have an abstract class Animal with a property sound. The default behaviour should be to complain that sound has to be defined in subclasses:
package Animal;
use namespace::autoclean;
use Moose;
has 'sound' => (
is => 'ro',
default => sub {
confess shift, " needs to define sound!"
}
);
1;
A subclass has to do nothing else than define sound:
package Horse;
use namespace::autoclean;
use Moose;
extends 'Animal';
sub sound { 'neigh' }
1;
But testing this with
use strict;
use warnings;
use 5.010;
use Horse;
my $talking = Horse->new;
say "The horse says ", $talking->sound, '.';
results in
Horse=HASH(0x3029d30) needs to define sound!
If I replace the anonymous function in Animal with something simpler as in
has 'sound' => (
is => 'ro',
default => 'something generic',
);
things work fine. Why is that? Why is the default function executed even though I override it in the subclass?
There's two things in play here: How attributes are initialized and how accessors work.
Non-lazy ('eager') attributes are initialized when the class is instantiated. That's why you can actually leave off the
say "The horse says ", $talking->sound, '.';
and get the same error. If you make the attribute lazy, on the other hand, the error goes away. And that leads us to the real reason: the difference between attributes, accessors, and builders.
Animal has an attribute, sound, which is just a place that stores some data related to instances of the class. Because sound was declared ro, Animal also has a method that acts as an accessor, confusingly also called sound. If you call this accessor, it looks at the value of the attribute and gives it to you.
But this value exists independent of the accessor. The accessor provides a way to get at the value, but the actual existence of the value is dependent on the attribute's builder. In this case, the builder for the attribute is the anonymous method sub { confess shift, " needs to define sound!" }, and it will get run as soon as the attribute needs to have a value.
In fact, if you leave out the is => 'ro', you will stop Moose from creating an accessor at all, and the error will still pop at construction time. Because that's when your class builds the sound attribute.
When the attribute needs its value depends on whether you've declared it as lazy or not. Eager attributes are given their values on object construction. It doesn't matter if there's an accessor, the builder gets called when the object is created. And in this case, the builder dies.
Lazy attributes are given their values the first time they are needed. The default accessor tries to get the value of the attribute, which causes the builder to fire, which causes the script to die. When you override sound, you replace the default accessor with one that doesn't call the builder and therefore doesn't die anymore.
Does that mean you should make the sound attribute lazy? No, I don't think so. There's better mechanisms available, depending on what exactly you are trying to assert. If what you are trying to assert is that Animal->sound must be defined, you can use BUILD like so:
package Animal;
use namespace::autoclean;
use Moose;
has 'sound' => (is => 'ro');
sub BUILD {
my ($self) = #_;
confess "$self needs to define sound!"
unless defined $self->sound;
}
1;
During object construction, each of the parent classes' BUILD methods gets called, which lets them make assertions about object state.
If, on the other hand, what you wanted to assert is that a subclass has to have overridden sound, it's better not to make sound an attribute at all. Instead,
package Animal;
use namespace::autoclean;
use Moose;
sub sound {
confess "Abstract method `sound` called!";
}
1;
Edit: Answer added below, question left here for historical purposes only.
Moose documentation states that:
If you want, you can also explicitly specify the method names to be
used for reading and writing an attribute's value. This is
particularly handy when you'd like an attribute to be publicly
readable, but only privately settable. For example:
has 'weight' => (
is => 'ro',
writer => '_set_weight',
);
This might be useful if weight is calculated based on other methods.
For example, every time the eat method is called, we might adjust
weight. This lets us hide the implementation details of weight
changes, but still provide the weight value to users of the class.
Based on this example, I wrote the following code:
has '_current_url' => (
is => 'ro',
isa => 'URI',
writer => '_write_URI_to_current_url',
);
# Thus we ensure only a URI object gets written to current_url always
sub _current_url
{
my $self = shift;
$self->_write_URI_to_current_url(URI->new_abs($_[0], $self->start_url));
}
My intention was to ensure that setting current_url always sets it to a URI object even if it was called with a simple string. However, when I try to run the code, I get:
Cannot assign a value to a read-only accessor of _current_url
at the place (within my class) where I'm trying to set the current_url (with $self->_current_url($str);).
This is my second day with Moose so I'm quite confused as to what's going on here. To my understanding the is => 'ro' only asks Moose not to create a default write accessor with the same name as the attribute, is that understanding correct? How can I achieve my goal here?
Ok, I believe I've found the issue.
The _current_url method I've pasted above got overridden by Moose's generated read-only accessor of the same name, so when I tried to call $self->_current_url with a value, it throws the above error to indicate that the read-only accessor cannot set values.
I guess the error message should ideally be Cannot assign a value **through** a read-only accessor of _current_url, not **to** a read-only accessor.
Changing the name of the sub to _set_current_url solved the problem. I guess another way to achieve the same would be to tell Moose that _current_url is => 'rw' and then create an around '_current_url'. I haven't tried this approach.
Late to the game but just saw this post and ran into the same thing a while back. '_set_current_url' really seems like an attribute accessor instead of an attribute. Might want to consider:
has '_current_url' => {
is => 'rw',
isa => 'URI',
writer => 'set_current_url',
reader => 'get_current_url'
}
A bit cleaner this way since the attribute is the original '_current_url' and you have accessors to get/set the attribute.
I'm Trying to extend a non-moose class, and when I call an accessor defined by moose for my extended class I'm getting the following error:
Not a HASH reference at accessor MyGraph::weight (defined at MyGraph.pm line 8) line 8
This is the simplified code:
package MyGraph;
use Moose;
use MooseX::NonMoose;
extends 'Graph';
has 'weight' => (
is => 'ro',
isa => 'Num',
);
no Moose;
__PACKAGE__->meta->make_immutable;
package main;
my $g = MyGraph->new;
$g->weight();
MooseX::NonMoose doesn't, out of the box, enable you to subclass a non-hashref class, and Graph uses an arrayref for its instances. The docs mention this, and suggest using MooseX::InsideOut to enable compatibility with non-moose classes that have other instance types.
The reference that the non-Moose class uses as its instance type must match the instance type that Moose is using. Moose's default instance type is a hashref.
Graph uses ARRAYREF as its instance type. MooseX::InsideOut is the solution.
package MyGraph;
use Moose;
use MooseX::InsideOut;
use MooseX::NonMoose;
extends 'Graph';
I've never done this but this looks like it might be what you want. http://metacpan.org/pod/MooseX::NonMoose
I'm an absolute newbie to Moose and so far I have read Moose and most of the Cookbook.
There a few things I don't get. I created the following package:
package MyRange;
use Moose;
use namespace::autoclean;
has [ 'start', 'end' ] => (
is => 'ro',
isa => 'Int',
required => 1,
);
__PACKAGE__->meta->make_immutable;
1;
Then:
use MyRange;
my $br = MyRange->new(
start => 100,
end => 180
);
Now I can access my fields using e.g. $br->{start}, but I can also modify them (although they are "read only") using e.g. $br->{start}=5000. I can also add new keys like $br->{xxx}=111.
Am I missing anything? Isn't the object protected in some way? What's the meaning of ro?
When you said is => 'ro' you told Moose to create read-only accessors for you, that is, a reader method. You call that as
$br->start;
or
$br->end;
Setting the attributes using those methods will result in an exception:
$br->start(42);
If you had used is => 'rw', then the above would work and update the attribute's value.
What you're doing is direct hash access on the object, which violates encapsulation and shouldn't ever be necessary when using Moose.
The Moose manual, i.e. all the documents under the Moose::Manual namespace explain that in detail. A good starting point for questions like this is probably Moose::Manual::Attributes.
When you access to the attribute with $br->{start}, you are bypassing the accessor and you are adressing directly the underlying Moose implementation. You can do it, but you are not supposed to. Also, if Moose changes the implementation, your code will break.
You should instead access the attribute using the accessor method:
my $start = $br->start;
When you say that the attribute is 'RO', it means you are not allowed to change the attribute value using the accessor:
$br->start(32);
I define a method inside a parametrized role that needs to create a new class at run time
using Moose::Meta::Class->create and apply that exact parametrized role to it. I am also making a new method for that role using
$new_class->meta->add_method( some_name => sub {
my ($self) = #_;
...
})
inside the sub {...} I want to access a method of the consumer class and use it for something, I have tried using $self->get_method, it didn't work, how do I do this?
Please notice that the $self inside the sub above is MooseX::Role::Parameterized::Meta::Role::Parameterizable
I also have another question, if I do this:
my $object = Moose::Meta::Class->create(
"some_type",
);
Why isn't $object of type some_type and it's some ugly MooseX::Role::Parameterized::Meta::Role::Parameterizable and how do I get to the object of type some_type?
To answer your second question, the reason is because Perl's OO doesn't allow you to add a method to just one instance of a class, so Moose has to fake it by creating a subclass with the extra method and reblessing the unique object into that subclass.
Note that, if you are doing things correctly and doing your introspection with isa, has, and/or does rather than by trying to rely on the name of the object's blessed package, this doesn't matter. The object still isa some_type, has all of some_type's attributes, and does all of some_type's roles even though it's now blessed into a package with an ugly auto-generated name.
It sounds like your underlying problem is nearly exactly what I described at this question: from within the role definition, you need to get at the class (and its meta-class) of the object or class the role is being applied to. This isn't possible from within normal roles, but it's possible through parameterized roles.
I'm not quite sure what you're trying to do here. Let's assume you have
my $new_class = Moose::Meta::Class->create('FooBar');
then $new_class is the meta object for FooBar. So, if you want to add a method to FooBar you would say
$new_class->add_method(foo => sub { … });
which would basically be the same as
FooBar->meta->add_method(foo => sub { … });
You should also probably use find_meta() from Moose::Util. This will return the correct meta object (if there is one) even if your class doesn't have a meta method or it uses it for something else.
As said, I'm not sure this answers your question.