In Moose, how do I set multiple defaults with one method call?
Asked Answered
G

3

10

I have two object attributes which require expensive calculations, so I'd like them to be lazy. They're most efficiently calculated together, so I'd like to calculate them at the same time. Does Moose provide a way to do this?

What I'd like is something like 'default' or 'builder' but instead of returning the default value it directly sets attributes. The return value would be ignored.

has max_things =>
    is      => 'rw',
    isa     => 'Int',
    lazy    => 1,
    xxxxx   => '_set_maxes';

has max_pairs =>
    is      => 'rw',
    isa     => 'Int',
    lazy    => 1,
    xxxxx   => '_set_maxes';

# Let's just assume this is an expensive calculation or the max_*
# attributes are used rarely and a lot of objects are created.
sub _set_maxes {
    my $self = shift;

    if( $self->is_32_bit ) {
        $self->max_things(2**31);
        $self->max_pairs(12345 * 2);
    }
    else {
        $self->max_thing(2**63);
        $self->max_pairs(23456 * 2);
    }

    return;
}

NOTE: I could write my own 'reader' or use 'around', but I'd rather keep it declarative and let Moose do the work. I also could make a new object just to store the paired values, but it seems like overkill for just two values.

Gasparo answered 6/3, 2014 at 20:9 Comment(0)
B
8

I wouldn't say this is especially elegant, but it works...

use v5.14;
use warnings;

package Goose {
    use Moose;

    has max_things => (
        is      => 'rw',
        isa     => 'Int',
        lazy    => 1,
        default => sub { shift->_build_maxes->max_things },
    );

    has max_pairs => (
        is      => 'rw',
        isa     => 'Int',
        lazy    => 1,
        default => sub { shift->_build_maxes->max_pairs },
    );

    sub is_32_bit { 1 }

    sub _build_maxes {
        my $self = shift;

        warn "Running builder...";

        if( $self->is_32_bit ) {
            $self->max_things(2**31);
            $self->max_pairs(12345 * 2);
         }
         else {
            $self->max_thing(2**63);
            $self->max_pairs(23456 * 2);
        }

        $self;  # helps chaining in the defaults above
    }
}

my $goose = Goose->new;
say $goose->max_things;
say $goose->max_pairs;
Behavior answered 6/3, 2014 at 22:52 Comment(3)
Only calls the builder once, doesn't store extra data, and the shim code is pretty minimal. Thanks, that's the best work around I've seen yet.Gasparo
A minor variation on this is to return $self from _build_maxes, so the default sub turns into shift->_build_maxes->max_things. I find it visually cleaner.Sucy
Thanks @DiabJerius, that is nicer, so I've incorporated it into my answer.Behavior
A
6

I usually handle this by directing both attributes at a third hidden attribute:

has 'max_things' => (
    'is'      => "rw",
    'isa'     => "Int",
    'lazy'    => 1,
    'default' => sub { (shift)->_both_maxes->{'max_things'} },
);

has 'max_pairs' => (
    'is'      => "rw",
    'isa'     => "Int",
    'lazy'    => 1,
    'default' => sub { (shift)->_both_maxes->{'max_pairs'} },
);

has '_both_maxes' => (
    'is'      => "ro",
    'isa'     => "HashRef",
    'lazy'    => 1,
    'builder' => "_build_both_maxes",
);

sub _build_both_maxes {
    my $self = shift;

    my ($max_things, $max_pairs);
    if($self->is_32_bit) {
        $max_things = 2 ** 31;
        $max_pairs = 12345 * 2;
    }
    else {
        $max_things = 2 ** 63;
        $max_pairs = 23456 * 2;
    }

    return {
        'max_things' => $max_things,
        'max_pairs'  => $max_pairs,
    };
}
Americano answered 6/3, 2014 at 21:47 Comment(2)
You avoid calling _build_both_maxes twice by caching the result in $self->_both_maxes. A clever work around. The double the memory doesn't sit well with me, and that fake attribute is going to be puzzling to the next maintainer.Gasparo
@Gasparo Yeah I'm not in love with it either, but it's the best solution I've come across. Hopefully some competing answers come along.Americano
A
3

Unless they specifically need to be distinct attributes, I generally use native attribute traits to "emulate" multiple attributes:

has config => (
  traits => ['Hash'],
  is => 'bare',
  isa => 'HashRef[Str]',
  lazy => 1,

  # returns a hashref of key/value config pairs
  builder => 'load_config',

  handles => {

    has_author => [ exists => 'author' ],
    author     => [ get    => 'author' ],
    has_email  => [ exists => 'email'  ],
    email      => [ get    => 'email'  ],
  },
);

This way the expensive builder simply needs to return a hashref with the keys 'author' and 'email' populated; the attribute will generate accessor methods that then look and feel like those of individual attributes. If you need to set them individually at new() this might not be the best option, though you can use BUILDARGS() to help; YMMV.

see also http://wps.io/2012/05/simulating-multiple-lazy-attributes/

Acetophenetidin answered 10/3, 2014 at 20:14 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.