Maybe not
"What's this Maybe
here for?" asked the Wise Old Elf. He was looking at the new Moose code that Syllabub Fizzyboughs had written for the new and improved Sleigh operating system.
has gps_unit => (
is => 'ro',
isa => Maybe[class_type('GPSUnit')],
);
"You see, Wise Old Elf, the Maybe
says that the value can either be whatever is in the brackets or can be undef
. So this means the value has to be something that's a GPS unit or undef - in case there's no GPS unit attached, you see.", Syllabub explained.
my $basic_sleigh => Sleigh->new(
name => 'Mk I'
);
# prints nothing!
say $basic_sleigh->gps_unit->name
if defined $basic_sleigh->gps_unit;
my $fancy_sleigh => Sleigh->new(
name => 'Mk II',
gps_unit => GPSUnit->new(
name => 'Super GPS 2001',
),
);
# prints 'Super GPS 2001'
say $fancy_sleigh->gps_unit->name
if defined $fancy_sleigh->gps_unit;
"Yes, yes. I know what Maybe
does. But I asked what it's there for. Haven't you heard the expression 'Maybe is a code smell?'"
"No Wise Old Elf, can't say that I have. To be honest, I'm not sure what you mean by code smell. My keyboard smells of freshly baked cookies like any other workstation at the North Pole."
"A code smell, by young elf, is a pattern that indicates that probably there's something wrong with your code. Other people call them 'Red Flags'. In this case it's because nine times out of ten, if you're using a Maybe
you're better off using a predicate"
has gps_unit => (
is => 'ro',
isa => class_type('GPSUnit'),
predicate => 'has_gps_unit',
);
"Or, if you're using MooseX::AttributeShortcuts"
has gps_unit => (
is => 'ro',
isa => class_type('GPSUnit'),
predicate => 1,
);
"So now you can use the has_gps_unit
method"
# prints 'Super GPS 2001'
say $fancy_sleigh->gps_unit->name
if defined $fancy_sleigh->has_gps_unit;
MooseX::LazyRequire
"Okay", Syllabub asked, "why would I want to do that?"
The Wise Old Elf explained, for starters, it's a lot more readable with exactly what's going on when you use the has_gps_unit
method. But what really blew Syllabub's mind was when the Wise Old Elf showed him MooseX::LazyRequires.
Even with a predicate you can still read from an attribute that hasn't been set in the constructor, and you'll get undef
back. This can result in hard to debug problems, where the value is taken, passed somewhere else, then much later in your code when something tries to access it...
# in Sleigh
sub fly_to_destination {
my $self = shift;
my $place = shift;
my $trip = Trip->new(
start => $self->current_location,
end => $place,
gps => $self->gps_unit,
);
$trip->route($self);
}
This leads to the really unreadable error message from deep within the Trip
class's route
method if the gps_unit
attribute isn't set and returns undef
when fly_to_destination
is called. What you really need to do is throw an exception as soon as you try reading from an accessor that isn't set. That's what MooseX::LazyRequires does for any attribute that has the lazy_requires
parameter enabled:
use MooseX::LazyRequire;
has gps_unit => (
is => 'ro',
predicate => 1,
lazy_required => 1,
);
Which now, if you don't set the gps_unit
prior to calling fly_to_destination
, immediately generates a much more readable error message:
Attribute 'gps_unit' must be provided before calling reader
at /opt/perl/lib/site_perl/5.22.0/MooseX/LazyRequire/Meta/Attribute/Trait/LazyRequire.pm line 34.
MooseX::LazyRequire::Meta::Attribute::Trait::LazyRequire::__ANON__(Sleigh=HASH(0x7fd3a1807908))
called at reader Sleigh::gps_unit (defined at Sleigh.pm line 7) line 6
Sleigh::gps_unit(Sleigh=HASH(0x7fd3a1807908))
called at Sleigh.pm line 18
Sleigh::fly_to_destination(Sleigh=HASH(0x7fd3a1807908))
called at example.pl line 6
Syllabub was convinced and made the changes to all of his code.
MooseX::UndefTolerant::Attribute
"Wise Old Elf, I've got a problem", Syllabub explained the next day, "I changed all my code and now it's broken wherever I pass in undef
"
my $sleigh => Sleigh->new(
name => 'Mk III',
gps_unit => $factory->gps_unit, # might return undef if there isn't one
);
"We do this all over the place. Do I have to change all my code?". Syllabub showed the Wise Old Elf what he'd been "forced" to write:
my $gps = $factory->gps_unit;
if ($gps) {
$sleigh = Sleigh->new( name => 'Mk III', gps_unit => $gps );
} else {
$sleigh = Sleigh->new( name => 'Mk III' );
}
The Wise Old Elf explained that he could probably make his life a lot easier with the ternary operator:
my $gps = $factory->gps_unit;
my $sleigh = Sleigh->new(
name => 'Mk III',
($gps ? ( gps_unit => $gps ) : ()),
);
But there was another simpler strategy: Use MooseX::UndefTolerant::Attribute to allow undef
to mean the same thing as we didn't pass a value.
use MooseX::UndefTolerant::Attribute;
has gps_unit => (
traits => [ UndefTolerant ],
is => 'ro',
predicate => 1,
lazy_required => 1,
);
...
# this now works fine, as if gps_unit hadn't been passed
my $sleigh => Sleigh->new(
name => 'Mk IV',
gps_unit => undef,
);
Cleaner Code All Round
Syllabub was happy. His code was a lot easier to debug, and he'd learned something from the Wise Old Elf. Code review might be painful, but everything was a lot better afterward.