porton porton - 2 months ago 15
Perl Question

Automatic conversion of empty string to undef in Perl Moo objects

For some fields of a Perl Moo object I want to replace empty string when it is assigned to the field with

undef
.

That is I want:
$obj->x("")
to make the field
x
undefined.

Please help to develop a Moo extension which does this.




A possible way to do this:

sub make_field_undef {
my ($class, $field_name) = @_;
eval "package $class";
around $field_name => sub {
my $orig = shift;
my $self = shift;
my @args = @_;
if(@args >= 1) {
$args[0] = undef if defined $args[0] && $args[0] eq '';
}
$orig->($self, @args);
};
}


But are there "more structured" or "more declarative" ways to do this? Any other ways to do this?




A complete example with my implementation of it follows. But running it produces errors which I do not understand:

package UndefOnEmpty;
use Moo;

sub auto_undef_fields { () }

sub make_fields_undef {
my ($class) = @_;
eval "package $class";
around [$class->auto_undef_fields] => sub {
my $orig = shift;
my $self = shift;
my @args = @_;
if(@args >= 1) {
$args[0] = undef if defined $args[0] && $args[0] eq '';
}
$orig->($self, @args);
};
around 'BUILD' => {
my ($self, $args) = @_;
foreach my $field_name ($class->auto_undef_fields) {
$args->{$field_name} = undef if defined $args->{$field_name} && $args->{$field_name} eq "";
}
};
}

1;


Usage example:

#!/usr/bin/perl

package X;
use Moo;
use lib '.';
extends 'UndefOnEmpty';
use Types::Standard qw(Str Int Maybe);
use Data::Dumper;

has 'x' => (is=>'rw', isa=>Maybe[Str]);
has 'y' => (is=>'rw', isa=>Maybe[Str]);

sub auto_undef_fields { qw(x y) }
__PACKAGE__->make_fields_undef;

my $obj = X->new(x=>"");
$obj->y("");
print Dumper $obj->x, $obj->y;


Here are the errors:

$ ./test.pl
"my" variable $class masks earlier declaration in same scope at UndefOnEmpty.pm line 20.
"my" variable $args masks earlier declaration in same statement at UndefOnEmpty.pm line 21.
"my" variable $field_name masks earlier declaration in same statement at UndefOnEmpty.pm line 21.
"my" variable $args masks earlier declaration in same statement at UndefOnEmpty.pm line 21.
"my" variable $field_name masks earlier declaration in same statement at UndefOnEmpty.pm line 21.
syntax error at UndefOnEmpty.pm line 20, near "foreach "
Compilation failed in require at /usr/share/perl5/Module/Runtime.pm line 317.


Please help to understand what are the causes of the errors.

Answer

Why not use a coercion with the coerce attribute in has? That seems to be the most simple and straightforward way.

package Foo;
use Moo;

has bar => (
    is     => 'rw',
    coerce => sub { $_[0] eq q{} ? undef : $_[0] },
);

Here's what objects look like then.

package main;
use Data::Printer;
p my $foo1 = Foo->new( bar => q{} );
p my $foo2 = Foo->new( bar => 123 );
p my $foo3 = Foo->new;

__END__

Foo  {
    Parents       Moo::Object
    public methods (2) : bar, new
    private methods (0)
    internals: {
        bar   undef
    }
}
Foo  {
    Parents       Moo::Object
    public methods (2) : bar, new
    private methods (0)
    internals: {
        bar   123
    }
}
Foo  {
    Parents       Moo::Object
    public methods (2) : bar, new
    private methods (0)
    internals: {}
}

In Moose you can also define your own type that derives from your Str and has a built-in coercion. Then you can make all your attributes of that type and turn coercion on.

To make Moo behave like that, look at the documentation of has and the attribute isa, which is right above coerce (linked above).