Search code examples
perlmoose

Moose: How to get an array of objects? Traits?


I'm beginning to realize that this is for beginners:

package Bad;

has 'arr' => ( is => 'rw', 'ArrayRef[Str]' );

package main;

my $bad = Bad->new(arr => [ "foo", "bar" ]);
print $bad->arr->[0], "\n";

Enter traits. I'm underwhelmed by the traits API, though. Have I misunderstood something? Can I get this API instead somehow? :

print $bad->arr->get(0), "\n";

Details

Review the canonical traits example from Moose::Meta::Attribute::Native::Trait::Array

package Stuff;
use Moose;

has 'options' => (
    traits  => ['Array'],
    is      => 'ro',
    isa     => 'ArrayRef[Str]',
    default => sub { [] },
    handles => {
        all_options    => 'elements',
        add_option     => 'push',
        map_options    => 'map',
        filter_options => 'grep',
        find_option    => 'first',
        get_option     => 'get',
        join_options   => 'join',
        count_options  => 'count',
        has_options    => 'count',
        has_no_options => 'is_empty',
        sorted_options => 'sort',
    },
);

no Moose;
1;

An object declared like that is used e.g.:

my $option = $stuff->get_option(1);

I really don't like that for one array attribute I get and have to manually name 11 methods in my Stuff class - one for every single operation that one can do to 'options'. Inconsistent naming is bound to happen and it is bloaty.

How do I instead (elegantly) get an API like:

my $option = $stuff->options->get(1);

Where all the methods from Moose::Meta::Attribute::Native::Trait::Array are implemented in a type-safe way?

Then all the operations on every single Array are named in exactly the same way...

(I'm actually using Mouse, but most of Mouse is identical to Moose)


Solution

  • I think that the best way to get your API into that format would be to create a new object for the options, and delegate the methods into it directly. Something like:

    package Stuff;
    use Moose;
    use Stuff::Options;
    
    has 'options' => (
        'is'      => "ro",
        'isa'     => "Stuff::Options",
        'default' => sub { Stuff::Options->new },
    );
    
    no Moose;
    1;
    

    And then in Stuff/Options.pm:

    package Stuff::Options;
    use Moose;
    
    has '_options' => (
        'is'      => "ro",
        'isa'     => "ArrayRef[Str]",
        'traits'  => [ "Array" ],
        'default' => sub { [] },
        'handles' => [ qw(elements push map grep first get join count is_empty sort) ],
    );
    
    no Moose;
    1;
    

    This would allow code as in your example to work ($stuff->options->get(1)).