2017 twenty four merry days of Perl Feed

Maybe not

MooseX::LazyRequire, MooseX::UndefTolerant::Attribute - 2017-12-19

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


1: 
2: 
3: 
4: 

 

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.


1: 
2: 
3: 
4: 
5: 
6: 
7: 
8: 
9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
18: 

 

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"


1: 
2: 
3: 
4: 
5: 

 

has gps_unit => (
    is => 'ro',
    isa => class_type('GPSUnit'),
    predicate => 'has_gps_unit',
);

 

"Or, if you're using MooseX::AttributeShortcuts"


1: 
2: 
3: 
4: 
5: 

 

has gps_unit => (
    is => 'ro',
    isa => class_type('GPSUnit'),
    predicate => 1,
);

 

"So now you can use the has_gps_unit method"


1: 
2: 
3: 

 

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


1: 
2: 
3: 
4: 
5: 
6: 
7: 
8: 
9: 
10: 
11: 
12: 
13: 

 

# 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:


1: 
2: 
3: 
4: 
5: 
6: 
7: 

 

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"


1: 
2: 
3: 
4: 

 

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:


1: 
2: 
3: 
4: 
5: 
6: 

 

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:


1: 
2: 
3: 
4: 
5: 

 

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.


1: 
2: 
3: 
4: 
5: 
6: 
7: 
8: 
9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 

 

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.

Gravatar Image This article contributed by: Mark Fowler <mark@twoshortplanks.com>