http://www.perlmonks.org?node_id=1019944


in reply to Re: Moose and default values
in thread Moose and default values

Good idea. I don't know why I didn't think of initializers. The Moose cabal aren't especially big fans of initializers, and consider them to be a bit of a misfeature, but this is the kind of niche area they seem to come in handy.

Let's take your initializer technique and add some metaprogramming.

First some test cases...

use v5.14; use Test::More; package Foo { use Moose; has foo => ( traits => ['MooseX::IgnoreFalse::Trait::Attribute'], is => 'ro', default => 'whatever', ); } is(Foo->new(foo => 'xyz')->foo, 'xyz'); is(Foo->new(foo => '0')->foo, 'whatever'); is(Foo->new(foo => '')->foo, 'whatever'); is(Foo->new(foo => undef)->foo, 'whatever'); done_testing;

When those tests all pass, then we're done.

Now, a first stab at the metatrait. For attribute metatraits, after _process_options can be a very handy place to hook onto. It effectively allows you to rewrite a has declaration before the attribute gets created. If you're not doing anything to deeply modify how Moose internals work, then that's often sufficient.

Here we'll just hook after _process_options to add an initializer that does the following:

  1. If the value passed into the constructor was true, then set it.
  2. Otherwise, if there is a default, set the attribute value to the default value.
  3. Otherwise, if there is a builder, set the attribute value to the built value.
  4. Otherwise, don't set the attribute at all.

How does that look?

use v5.14; package MooseX::IgnoreFalse::Trait::Attribute { use Moose::Role; after _process_options => sub { my ($class, $name, $options) = @_; $options->{initializer} = sub { my ($instance, $value, $setter) = @_; my $meta = $instance->meta->get_attribute($name); $value ? $setter->( $value ) : $meta->has_default ? $setter->( $meta->default($instance) +) : $meta->has_builder ? $setter->( $meta->_call_builder($inst +ance) ) : (); } }; }

This is sufficient to pass our test case; yay! However, the keen eyed may have noticed that if the attribute already had an initializer, that will be replaced with our new initializer. So let's make our hook a little smarter to supplement any existing initializer...

use v5.14; package MooseX::IgnoreFalse::Trait::Attribute { use Moose::Role; after _process_options => sub { my ($class, $name, $options) = @_; if (my $orig = $options->{initializer}) { $options->{initializer} = sub { my ($instance, $value, $setter) = @_; my $meta = $instance->meta->get_attribute($name); $value ? $orig->( $instance, $value, $sett +er ) : $meta->has_default ? $orig->( $instance, $meta->defaul +t($instance), $setter ) : $meta->has_builder ? $orig->( $instance, $meta->_call_ +builder($instance), $setter ) : (); } } else { $options->{initializer} = sub { my ($instance, $value, $setter) = @_; my $meta = $instance->meta->get_attribute($name); $value ? $setter->( $value ) : $meta->has_default ? $setter->( $meta->default($instan +ce) ) : $meta->has_builder ? $setter->( $meta->_call_builder($ +instance) ) : (); } } }; }

That's a fairly gentle introduction to attribute metaprogramming.

For completeness, here's everything in one file.

package Cow { use Moo; has name => (is => 'lazy', default => sub { 'Mooington' }) } say Cow->new->name

Replies are listed 'Best First'.
Re^3: Moose and default values
by 7stud (Deacon) on Feb 22, 2013 at 00:06 UTC

    The Moose cabal aren't especially big fans of initializers, and consider them to be a bit of a misfeature,

    Why is that? The only thing I can find is the following from Moose::Manual:BestPractices:

    Don't use the initializer feature
    
    Don't know what we're talking about? That's fine.
    
    Uh, okay, but I do know what you are talking about. The manual's reasoning for that best practice doesn't quite rise to the level of a Damian Conway best practice.

      "Why is that?"

      Good question. I wasn't sure myself. I seem to recall RJBS saying that initializers were his first ever contribution to Moose, but that he regretted ever adding them.

      The best reasons I could come up with were:

      • It leads to inconsistent behaviour when an attribute value is set via the constructor versus a writer method. (Of course, this isn't an issue for read-only attributes.)

      • Initializers can't be inlined whereas type coercions can be partly compiled into a more efficient form, and have potential for improvement.

      Having asked in #moose, I've had the following additional suggestions:

      • The initializer isn't called until after type constraints have been checked, so initializers don't work for one of the main use cases you'd expect them to be useful for: to munge values to conform to their type constraints.

      • (I'll add to this list when I receive more responses from IRC.)

      package Cow { use Moo; has name => (is => 'lazy', default => sub { 'Mooington' }) } say Cow->new->name