Enumerations in Moose

It's quite a common pattern in object-oriented programming to have an attribute which takes a string as its value, but which only has a small number of valid values. For example:

 package Shirt;
 use Moose;
 
 # "S", "M", "L", or "XL"
 has size => (is => 'ro', isa => 'Str', required => 1);

This offers no protection against invalid string values.

 # No exception is thrown
 my $shirt = Shirt->new(size => "LX");

Moose can do some validation for you:

 package Shirt;
 use Moose;
 use Moose::Util::TypeConstraints;
 
 enum ClothesSize => [qw/ S M L XL /];
 
 has size => (is => 'ro', isa => 'ClothesSize', required => 1);

That protects us from one class of error. Here's another:

 if ( $shirt->size eq "LX" ) {
    $price += 2.50;
 }

Oh dear, that conditional is never going to be true, is it? How can we protect against that sort of error?

 package Shirt;
 use Moose;
 use Moose::Util::TypeConstraints;
 
 enum ClothesSize => [qw/ S M L XL /];
 
 has size => (is => 'ro', isa => 'ClothesSize', required => 1);
 
 sub is_S  { shift->size eq "S"  }
 sub is_M  { shift->size eq "M"  }
 sub is_L  { shift->size eq "L"  }
 sub is_XL { shift->size eq "XL" }

Now our price calculation can be:

 if ( $shirt->is_LX ) {
    $price += 2.50;
 }

To err is human: we've still misspelt it, but at least an exception will be thrown.

Now our simple little string attribute has become a lot more complex. How can we bring it back under control?

MooseX::Enumeration

First, we install MooseX::Enumeration. Then we use it:

 package Shirt;
 use Moose;
 has size => (
    traits   => ['Enumeration'],
    is       => 'ro',
    enum     => [qw/ S M L XL /],
    handles  => 1,
    required => 1,
 );

This does everything from the previous example, including defining is_S, is_M, is_L, and is_XL methods.

The Bare Necessities

Now, here's a trick. It may be that because of our wonderful new is_* methods, we never want to call $shirt->size again. In fact, it's possible to tell Moose to never create that accessor method:

 package Shirt;
 use Moose;
 has size => (
    traits   => ['Enumeration'],
    is       => 'bare',
    enum     => [qw/ S M L XL /],
    handles  => 1,
    required => 1,
 );

Now the shirt size can be set through the constructor as usual, but can only be queried via the is_* interface.

1 Comment

Nice! I wouldn't mind at all if this became Moose::Meta::Attribute::Native::Trait::Enum. Nothing would break, and the entire ecosystem can benefit immediately.

Leave a comment

About Toby Inkster

user-pic I'm tobyink on CPAN, IRC and PerlMonks.