Sunday, 17 December 2017

Moo: One approach to terminating the process when attributes of a dynamically assigned role are missing

The value of a role’s required attribute is only checked on object creation. This article provides one way of enforcing it when applying the role to an object on the fly.

Picture of a Dalek

Introduction

What is certain about Perl programming is that there's always more to learn, and this article on applying roles in OO Perl is one such example. It's the challenge of ensuring attributes are set in roles applied on the fly.

It’s quite often the case that if people forget to assign a value to an attribute we really want things to die rather than have undef being returned as the value of the attribute.

Here's an example:
{
package Brand;
use Moo::Role;
has name => ( is => 'ro' );
sub description { "This is a nice ". shift->name }
}
{
package Car;
use Moo;
with 'Brand';
}
say Car->new ( name => "Peugeot" )->description; # Good usage
say Car->new->description; # Bad usage
view raw car-01.pl hosted with ❤ by GitHub
and when you run it:

> ./car-01.pl
This is a nice Peugeot
Use of uninitialized value in concatenation (.) or string at ./car-01.pl line 15.
This is a nice
view raw car-01.out hosted with ❤ by GitHub
In order to terminate the process so that the script behaves as follows:

> ./car-02.pl
This is a nice Peugeot
Missing required arguments: name at (eval 12) line 49.
view raw car-02.out hosted with ❤ by GitHub
all you need to do is make the "name" attribute "required":

has name => ( is => 'ro', required => 1 );
# This will die: my $car = Car->new;
view raw car-02.pl hosted with ❤ by GitHub
This all works fine and dandy until you decide not to apply the role on creation of the object. In this example, it might be that a car can have a brand, or it might be home-made with no brand. At this point you have to remove the role declaration from the class and apply the role to the object:

{
package Brand;
use Moo::Role;
has name => ( is => 'rw', required => 1 ); # made rw
sub description { "This is a nice ". shift->name }
}
{
package Car;
use Moo;
# with 'Brand'; # not at compile time
}
my $car = Car->new;
# TODO an investigation which determines that it really is a branded car
Moo::Role->apply_roles_to_object($car, 'Brand');
# $car->name("Peugeot");
say $car->description;
view raw car-03.pl hosted with ❤ by GitHub
As you can see from the following output - the "required" modifier isn't applied because that only happens on creation of the object.

./car-03.pl
Use of uninitialized value in concatenation (.) or string at ./car-03.pl line 15.
This is a nice
view raw car-03.out hosted with ❤ by GitHub

"Terminate!" by default

Rather than requiring the attribute on object creation (before the role has been applied) the solution here is to make the attribute lazy, then die by default on accessing the attribute to indicate that a value hasn't been assigned.

{
package Brand;
use Moo::Role;
has name => (
is => 'rw',
lazy => 1,
default => sub { die "name is required" }
);
sub description { "This is a nice ". shift->name }
}
{
package Car;
use Moo;
}
my $car = Car->new;
Moo::Role->apply_roles_to_object($car, 'Brand');
# $car->name("Peugeot"); # assign a name to avert termination!
say $car->description;
view raw car-04.pl hosted with ❤ by GitHub

Summary

A Moo role's "required" attribute isn't actually required if the role is being assigned to an object at runtime. One solution is to die by default when accessing the attribute with no assigned value.

Acknowledgements

This rabbit hole traversal was instigated by one of Dave Cross's rigorous code reviews.

No comments:

Post a Comment