Make the Moose constructor ignore undef arguments
Asked Answered
E

4

4

A hashtable is the typical initializer for your Perl objects. Now your input is unreliable in that you don't know whether for any given key there'll be a defined value, nor whether the key is there at all. Now you want to feed such unreliable input into Moose objects, and while absent keys are perfectly okay you do want to get rid of the undefined values so you don't end up with an object full of undefined attributes.

You could certainly take great care when instantiating objects and filter out the undefined values. But let's say you want to install that filter in your constructor because then it is in one place. You want the constructor to ignore undefined values, but not to die on encountering them.

For accessor methods, you can use around around to prevent the attribute to be set to undef. But those method modifiers aren't called for the constructor, only for accessors. Is there a similar facility in Moose to achieve the same effect for the c'tor, i.e. to preclude any undef attributes from being accepted?

Note that the Moose Any type will create the hash key in the object if the attribute is undef. I don't want that because I want %$self not to contain any undef values.

Here's some testing I did:

package Gurke;
use Moose;
use Data::Dumper;

has color  => is => 'rw', isa => 'Str', default => 'green';
has length => is => 'rw', isa => 'Num';
has appeal => is => 'rw', isa => 'Any';

around color => sub {
    # print STDERR Dumper \@_;
    my $orig = shift;
    my $self = shift;
    return $self->$orig unless @_;
    return unless defined $_[0];
    return $self->$orig( @_ );
};

package main;
use Test::More;
use Test::Exception;

my $gu = Gurke->new;
isa_ok $gu, 'Gurke';
diag explain $gu;
ok ! exists $gu->{length}, 'attribute not passed, so not set';
diag q(attempt to set color to undef - we don't want it to succeed);
ok ! defined $gu->color( undef ), 'returns undef';
is $gu->color, 'green', 'value unchanged';
diag q(passing undef in the constructor will make it die);
dies_ok { Gurke->new( color => undef ) }
    'around does not work for the constructor!';
lives_ok { $gu = Gurke->new( appeal => undef ) } 'anything goes';
diag explain $gu;
diag q(... but creates the undef hash key, which is not what I want);
done_testing;
Epinasty answered 28/4, 2011 at 19:4 Comment(1)
Read Moose::Cookbook. Specifically Moose::Cookbook::Basics::Recipe10.Skyward
H
13

This is exactly what MooseX::UndefTolerant does. If you make your class immutable, it will be much faster than writing your own BUILDARGS method, as the code is inlined into the generated constructor.

Hazaki answered 28/4, 2011 at 21:26 Comment(4)
Oh my oh my, there are so many Moose::WhatNot and MooseX::WhatElse modules ... And they do exactly what I want! :-) It'll take some more hanging around and using the toys, I guess. Thanks Ether, this is arguably superior to writing your own BUILDARGS, so I'm accepting this as the best answer.Epinasty
@Michael: :) If irc is your thing, you can find an excellent support network nearly 24/7 at irc.perl.org #moose. Oftentimes if there isn't an extension that does what you want, someone will buckle down and write one for you, just for kicks! :DHazaki
Thanks! Have actually never done any IRC, but this sounds like the occasion to give it a try. What client would I go with? Looks like tin is a classical choice, available for Cygwin. Also giving mIRC for Windows a try.Epinasty
@Michael when I was using windows I used the Win32 port of XChat. I now use irssi through an ssh+screen session on a remote box.Lipkin
S
5

Just provide your own BUILDARGS subroutine.

package Gurke;

...

around 'BUILDARGS' => sub{
  my($orig,$self,@params) = @_;
  my $params;
  if( @params == 1 ){
    ($params) = @params;
  }else{
    $params = { @params };
  }

  for my $key ( keys %$params ){
    delete $params->{$key} unless defined $params->{$key};
  }

  $self->$orig($params);
};
Skyward answered 28/4, 2011 at 19:17 Comment(1)
Excellent, thanks again, Brad! My first instinct was to replace keys with each, but of course you mustn't do that when calling delete during the iteration. Much appreciated!Epinasty
C
2

I realize that it is somewhat a duplicated effort, but you can hook ctor with BUILDARGS:

around BUILDARGS => sub {
    my $orig   = shift;
    my $class  = shift;
    my %params = ref $_[0] ? %{$_[0]} : @_;

    return $class->$orig(
        map  { $_ => $params{$_} }
        grep { defined $params{$_} }
        keys %params
    );
};

Edit: Edited to support even the reference passed to ctor.

Confute answered 28/4, 2011 at 19:18 Comment(1)
This won't work if somebody calls Class->new( {key => value} ) (i.e., passes a hashref instead of a list).Burack
T
0

While the example given clarifies that the question is inspired by a desire to handle undef attributes passed to a constructor, the question itself additionally implies the case of passing only undef to the constructor, which is something I've encountered and wanted to solve.

E.g., Class->new(undef).

I like bvr's BUILDARGS answer. It can be extended to handle the case of passing an undef value instead of a hashref as the lone argument to a constructor:

around BUILDARGS => sub {
    my $orig   = shift;
    my $class  = shift;
    my %params = defined $_[0] ? ref $_[0] ? %{$_[0]} : @_ : ();

    return $class->$orig(
        map  { $_ => $params{$_} }
        grep { defined $params{$_} }
        keys %params
    );
};

MooseX::UndefTolerant does not appear to support this case.

Tyler answered 19/9, 2014 at 16:4 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.