Hacking Thy Fearful Symmetry

One builder to rule them all

January 26th, 2018
perlmoose

One builder to rule them all

I've just punted MooseX-Attribute-Multibuilder to CPAN. It's a dirt-simple module, but it addresses a itch I had since forever.

I'm sure you had that problem a few times as well. You have this object with a few attributes, and the generation of their default values happens to be interconnected. For example, let's say you have an object pointing to a web page and recording its title and creation time. Naively, you can do something like this:


package MyPage;

use Moose;

use Web::MadeUp::Magic::Module qw/ fetch /;

use experimental 'signatures';

has url => (
    is       => 'ro',
    required => 1,
);

has title => (
    is   => 'ro',
    lazy => 1,
    default => sub($self) {
        fetch( $self->url )->h1;
    },
);

has creation_time => (
    is   => 'ro',
    lazy => 1,
    default => sub($self) {
        fetch( $self->url )->creation_time;
    },
);

It works, but we're hitting the url twice. Feels wasteful.

The solution, traditionally

The usual way around that is to have an intermediary private attribute holding the required data. Something like:


package MyPage;

use Moose;

use Web::MadeUp::Magic::Module qw/ fetch /;

use experimental 'signatures';

has url => (
    is       => 'ro',
    required => 1,
);

has _page => (
    is      => 'ro',
    lazy    => 1,
    default => sub($self) {
        fetch( $self->url )
    }
);

has title => (
    is      => 'ro',
    lazy    => 1,
    default => sub($self) {
        $self->_page->h1;
    },
);

has creation_time => (
    is   => 'ro',
    lazy => 1,
    default => sub($self) {
        $self->_page->creation_time;
    },
);

Or even, if page turns out to be too big to keep around:


package MyPage;

use Moose;

use Web::MadeUp::Magic::Module qw/ fetch /;

use experimental 'signatures';

has url => (
    is       => 'ro',
    required => 1,
);

has _page_info => (
    is      => 'ro',
    lazy    => 1,
    default => sub($self) {
        return {
            map {
                title         => $_->h1,
                creation_time => $_->creation_time,
            } fetch( $self->url )
        }
    }
);

has title => (
    is      => 'ro',
    lazy    => 1,
    default => sub($self) {
        $self->_page_info->{title};
    },
);

has creation_time => (
    is   => 'ro',
    lazy => 1,
    default => sub($self) {
        $self->_page_info->{creation_time};
    },
);

The multibuilder solution

The solutions from the previous section are fine, but they still require to set up that in-between attribute. That's more work than strictly necessary. Which is where the new attribute trait enters the picture. It introduces a new attribute option, multibuilder, that act just like builder, but instead of returning the value for a single attribute, it's expected to return a set of default values. In other words, the management of the intermediary state is taken care of for us.

As usual, an example would speak much eloquently than anything else:


package MyPage;

use Moose;
use MooseX::Attribute::Multibuilder;

use Web::MadeUp::Magic::Module qw/ fetch /;

use experimental 'signatures';

has url => (
    is       => 'ro',
    required => 1,
);

has [qw/ title creation_time /] => (
    traits       => [ 'Multibuilder' ],
    is           => 'ro',
    lazy         => 1,
    multibuilder => '_page_info',
);

sub _page_info($self) {
    return {
        map {
            title         => $_->h1,
            creation_time => $_->creation_time,
        } fetch( $self->url )
    }
}

Is it a huge change? Not really. But it's greasing a wheel that has been squeaking for me for a long time, and it's worth it.

Enjoy!

Seen a typo or an error? Submit an edit on GitHub!