Moose (Perl): convert undef to empty string or 0 rather than die()
Asked Answered
R

3

8

I've received a lot of exceptions from QA due to incomplete data being fed to my Moose constructors. The attribute name is present in the constructor arguments, but the value is undef.

It's a fact of life with many scripting applications that things are just undef. And oftentimes this is perfectly fine. You don't want an annoying warning from the warnings pragma (so you do no warnings 'uninitialized'), and you certainly don't want your code to die because one little value, say the housenumber, is undef.

So without further ado, I want my Moose constructors to behave like straight Perl (i.e. without use warnings 'uninitialized'), which is to convert undef to 0 or the empty string as required. The attempt shown in this sample does not work for the case where the attribute name is present but the value is undef. I could think of using BUILDARGS to achieve what I want. But is there a declarative way in plain Moose without resorting to MooseX::UndefTolerant (which unfortunately I cannot use as it is not installed)?

package AAA;
use Moose;
has 'hu', is => 'ro', isa => 'Str';
has 'ba', is => 'ro', isa => 'Int';
no Moose; __PACKAGE__->meta->make_immutable;

package BBB;
use Moose; extends 'AAA';
has '+hu', default => ''; # don't want to die on undef
has '+ba', default => 0;  # idem
no Moose; __PACKAGE__->meta->make_immutable;

package main;
use Test::More;
use Test::Exception;
# Those AAAs should die ...
throws_ok { AAA->new( hu => undef ) }
    qr/Validation failed for 'Str' with value undef/;
throws_ok { AAA->new( ba => undef ) }
    qr/Validation failed for 'Int' with value undef/;
# .. but these BBBs should live:
lives_ok  { BBB->new( hu => undef ) } 'hu supplied as undef';
lives_ok  { BBB->new( ba => undef ) } 'ba supplied as undef';
done_testing;
Ralph answered 23/6, 2011 at 16:57 Comment(4)
As far as I know, you shouldn't be trying to suppress warnings. Why don't you check if values are defined before hand? (defined($value)) Also, you could install MooseX::UndefTolerant locally to some directory $my_dir and use lib($my_dir);Tomtoma
The problem is type constraints. When you say foo isa Int, and you supply an undef for foo, it fails the type constraint because undef isn't an Int.Crescen
@YGomez +1 Nice idea, I might in fact be doing this. :-) As for warnings because of uninitialized values, see common::sense. I agree with the author Marc Lehmann in his discussion of uninitialized values. (There are exceptions, but hey - they're exceptions.) Checking the values beforehand is just cumbersome. I'd have to repeat the checks in many places and I try to stay DRY.Ralph
common::sense is no common sense.Arvillaarvin
G
10

In Moose::Manual::Types is a way documented to deal with exactly this kind of problem.

Use the Maybe[a] type.

package AAA;
use Moose;

has 'hu', is => 'ro', isa => 'Str';
has 'ba', is => 'ro', isa => 'Int';

no Moose; __PACKAGE__->meta->make_immutable;


package BBB;
use Moose; extends 'AAA';

has 'hu', is => 'rw', isa => 'Maybe[Str]', default => ''; # will not die on undef
has 'ba', is => 'rw', isa => 'Maybe[Int]', default => 0;  # idem

sub BUILD {
    my $self = shift;
    $self->hu('') unless defined $self->hu;
    $self->ba(0) unless defined $self->ba;
}

no Moose; __PACKAGE__->meta->make_immutable;


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

# Those AAAs should die ...
throws_ok { AAA->new( hu => undef ) }
    qr/Validation failed for 'Str' with value undef/;
throws_ok { AAA->new( ba => undef ) }
    qr/Validation failed for 'Int' with value undef/;

# .. but these BBBs should live:
lives_ok  { BBB->new( hu => undef ) } 'hu supplied as undef';
lives_ok  { BBB->new( ba => undef ) } 'ba supplied as undef';

my $bbb = BBB->new( hu => undef, ba => undef );

is $bbb->hu, '', "hu is ''";
is $bbb->ba, 0, 'ba is 0';

done_testing;
Grisham answered 23/6, 2011 at 18:4 Comment(4)
Thanks, this works. The undef values don't get autoconverted to 0 and the empty string, respectively, but I guess I'll have to put with up that. (Or write my own MooseX::UndefUpgrade extension.)Ralph
To set a value when undef is passed you could use the BUILD sub that gets invoked on object creation via BBB->new. Updated code accordingly. You would have to set the attributes 'rw' though.Grisham
Now I don't know how Moose sorts things out internally (I know, open source ...), but I might not be alone in thinking that it would stand to benefit from endowing the attribute declarator (has) with an onundef property. Looks like you could say that the default behaviour of onundef is to croak (or confess). But why not has 'something', is => 'ro', isa => 'Str', onundef => ''? Class::MOP::Attribute has default and initializer, but neither is suitable for the special case of handling undef.Ralph
I disagree on this one. I think the API should be kept as narrow as possible. The type system contains Undef and it can be validated and handled like the other types can. If an attribute is not set during construction the default value/callback will jump in. If it is set it will not. That is consistant behaviour and should not be changed for one type or the other. If you need to mangle the data passed in you can still use BUILD.Grisham
J
4

Your complaint really is that Moose is doing exactly what it is supposed to be doing. If you explicitly pass undef as a value, but that value can only be an Int, then you should get an error.

So you need to make a choice. You can either change the type (via union) to allow undef as a valid value like so:

    has 'hu', is => 'ro', isa => 'Str | Undef';
    has 'ba', is => 'ro', isa => 'Int | Undef';

Or you can just not send in undefined values:

    my %aa_params = ();
    $aa_params{hu} = $foo if defined $foo;

    $aa = AA->new( %aa_params );

Or finally, for some unknown reason you absolutely cannot resist sending in invalid undefined values for things which should not be explicitly set to undefined, just write a quick filter:

    sub filt_undef {
      my %hash = @_;
      return map { $_ => $hash{$_} } grep { defined $hash{$_} } keys %hash;
    }

    $aa = AA->new( filt_undef( hu => undef ) );

But this seems rather awkward and awful.

Jovial answered 23/6, 2011 at 18:4 Comment(1)
Your suggestion using a union works fine, too. The Maybe solution is a more idiomatic expression for the undef problem and hence to be preferred for this problem, I guess.Ralph
F
3

Or use on-the-fly coercion:

package BBB;
use Moose;
use MooseX::AttributeShortcuts;
extends 'AAA';
has '+hu',
  traits => [Shortcuts],
  coerce => [ Undef => sub { '' } ],
;
Ferrocene answered 10/3, 2014 at 20:39 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.