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;