Sue D. Nymme has asked for the wisdom of the Perl Monks concerning the following question:

I have a type constraint I've set up, called 'MyApp::DateOnly'. It's for a DateTime object with certain restrictions.

type 'MyApp::DateOnly' => as 'DateTime' => where { blessed($_) && $_->isa('DateTime') && $_->hour == 0 && $_->min == 0 && $_->sec == 0 };

I have also included a couple of simple coercions, so that the user can pass a properly-formatted string as a convenience, and so that DateTime objects are made to follow the limitations of the type:

coerce 'Search::DateOnly' => from 'DateTime' => via { $_->truncate(to => 'day') }; coerce 'Search::DateOnly' => from 'Str' => via { state $parser = DateTime::Format::Strptime->new( pattern => '%Y/%m/%d'); $parser->parse_datetime($_); };

This all works fine.

Now I need to allow undef as a null value for certain attributes. I thought I could set up such an attribute like this:

has foo => ( is => 'ro', isa => 'Maybe[MyApp::DateOnly]', coerce => 1, );

but it doesn't work -- not only does it fail on undef arguments, it won't even accept and coerce the Str or DateTime arguments it did before! Is Maybe[] processing not done automatically? How do I enable it for this type constraint?

Replies are listed 'Best First'.
Re: Moose type question
by ikegami (Patriarch) on Jun 07, 2010 at 18:27 UTC
    I faced the same problem and used:
    my $date_constraint = Moose::Util::TypeConstraints::find_type_constrai +nt('MyApp::DateOnly'); around BUILDARGS => sub { my $orig = shift; my $class = shift; my %args = ( @_ == 1 ? %{ $_[0] } : @_ ); # Maybe[] doesn't coerce $args{$foo} = $date_constraint->coerce($args{foo}) if defined($args{$foo}); return $class->$orig(%args); }; has foo => ( reader => 'get_foo', writer => '_set_foo', isa => 'Maybe[MyApp::DateOnly]', coerce => 1, handles => { set_foo => sub { my ($self, $arg) = @_; # Maybe[] doesn't coerce $arg = $date_constraint->coerce($arg) if defined($arg); $self->_set_foo($arg); }, }, );

    I use MooseX::FollowPBP which creates get_foo and set_foo accessors for is => 'rw'. If you use the default accessor style, adjust appropriately.


    As an aside, it seems to me that

    type 'MyApp::DateOnly' => where { blessed($_) && $_->isa('DateTime') && $_->hour == 0 && $_->min == 0 && $_->sec == 0 };

    would be more clear when written as

    subtype 'MyApp::DateOnly' => as 'DateTime' => where { $_->hour == 0 && $_->min == 0 && $_->sec == 0 };

      Thanks... I can see how your BUILDARGS/handles solution would work, but it sure is ugly. :-/ I was hoping there'd be a built-in way. Like, you know, Maybe[] handling it!

      Also, I like your tip for making my where clause more clear. Much simpler, thanks.

        [ Reported in https://rt.cpan.org/Ticket/Display.html?id=58387. ]

        If you care to file a bug report or add the missing functionality yourself, here's a test case:

        BEGIN { package Class; use strict; use warnings; use Moose; has val => ( is => 'rw', isa => 'Num', required => 1, ); use Moose::Util::TypeConstraints; coerce __PACKAGE__ => from 'Num' => via { __PACKAGE__->new(val => $_) }; } BEGIN { package Container; use strict; use warnings; use Moose; has always => ( is => 'rw', isa => 'Class', coerce => 1, default => sub { Class->new(val => 0) }, ); has maybe => ( is => 'rw', isa => 'Maybe[Class]', coerce => 1, default => undef, ); } use strict; use warnings; use Test::More tests => 15; my $always_fail = qr/^Attribute \(always\) does not pass the type cons +traint/; my $maybe_fail = qr/^Attribute \(maybe\) does not pass the type const +raint/; my $o = Class->new(val => 123); eval { Container->new() }; is($@, ''); eval { Container->new(always => $o) }; is($@, ''); eval { Container->new(always => 123) }; is($@, ''); eval { Container->new(always => "abc") }; like($@, $always_fail); eval { Container->new(maybe => undef) }; is($@, ''); eval { Container->new(maybe => $o) }; is($@, ''); eval { Container->new(maybe => 123) }; is($@, ''); eval { Container->new(maybe => "abc") }; like($@, $maybe_fail); my $c = Container->new(); eval { $c->always($o) }; is($@, ''); eval { $c->always(123) }; is($@, ''); eval { $c->always("abc") }; like($@, $always_fail); eval { $c->maybe(undef) }; is($@, ''); eval { $c->maybe($o) }; is($@, ''); eval { $c->maybe(123) }; is($@, ''); eval { $c->maybe("abc") }; like($@, $maybe_fail);

        Currently failing:

        eval { Container->new(maybe => 123) }; is($@, ''); eval { $c->maybe(undef) }; is($@, ''); eval { $c->maybe($o) }; is($@, ''); eval { $c->maybe(123) }; is($@, ''); eval { $c->maybe("abc") }; like($@, $maybe_fail);
Re: Moose type question
by stvn (Monsignor) on Jun 14, 2010 at 20:37 UTC

    You should take a look at MooseX::UndefTolerant, which is an extension to support a more liberal view on undefined values then the core Moose has. I can tell you right now that the Moose core will not be changing this behavior, but we have been discussing moving UndefTolerant into the core as an optional trait.

    -stvn

      I can tell you right now that the Moose core will not be changing this behavior

      Nor should it. That's why Maybe[] and MooseX::UndefTolerant exist. The problem is that Maybe[] doesn't currently work with coercion, and that's the behaviour that should be changed.

        The problem is that Maybe[] doesn't currently work with coercion, and that's behaviour that should be changed.

        The type 'Maybe[Foo]' is different the type 'Foo' and Maybe[`a] is a parameterized type, and parameterized types do not support deep coercion like that. We have discussed the topic of deep coercion, but it is simply deemed too magical and prone to action-at-a-distance type issues.

        Along with MooseX::UndefTolerant, it is also possible to subtype a Maybe[`a] type and then add coercions to that.

        subtype 'MaybeDateTime' => as 'Maybe[DateTime]'; coerce 'MaybeDateTime' => from 'St' => via { ... };
        Of course your coercions would need to account for the possibility of undef, but that shouldn't be too hard.

        -stvn

      Elsewhere, you posted http://article.gmane.org/gmane.comp.lang.perl.moose/1697

      which has two possible values, None, meaning no value, and Some(`a) meaning a value of the type `a

      That sounds like a union, so I tried it. Coercion works! ...but constraints don't?! Am I missing something?

      Simple demonstration:

      $ perl -Moose -e'has a=>(is=>"rw",isa=>"Undef",coerce=>1); __PACKAGE__ +->new(a=>"abc")' Attribute (a) does not pass the type constraint because: Validation fa +iled for 'Undef' failed with value abc at [snip backtrace] $ perl -Moose -e'has a=>(is=>"rw",isa=>"Undef|Undef",coerce=>1); __PAC +KAGE__->new(a=>"abc")' $ perl -Moose -e'has a=>(is=>"rw",isa=>"Undef|Undef",coerce=>0); __PAC +KAGE__->new(a=>"abc")' Attribute (a) does not pass the type constraint because: Validation fa +iled for 'Undef|Undef' failed with value abc at [snip backtrace]

      Full demonstration:

        That sounds like a union, so I tried it. Coercion works! ...but constraints don't?! Am I missing something?

        It is not a union, it is an algebraic data type, there is a HUGE difference in terms of type safety and soundness.

        Your second "Simple demonstration" seems to be a bug, I have brought it up in #moose-dev to discuss.

        UPDATE:
        Actually we figured out what is happening there, it is a fall through on an edge case. So if you look at this:

        perl -Moose -e'has a=>(is=>"rw",isa=>"Undef|Undef",coerce=>1); my $x = + __PACKAGE__->new(a=>"abc"); print $x->a'
        You will get Use of uninitialized value in print at -e line 3., which is happening because when Moose::Meta::TypeCoercion::Union attempts to coerce and doesnt find a proper coercion it returns 'undef'. Which in most cases would cause a type-check failure, but here actually passes the type check just fine.

        Funny, because this perfectly illustrates why I really don't like undef. In this case we are trying to use it as a "no value", but your test case is using it as a valid value itself. The two ways of using it do not mesh well at all.

        UPDATE(2):
        See also Semipredicate Problem.

        -stvn

      As its name implies, UndefTolerant only tolerates undef temporarily, whereas Maybe indicates undef is a valid value.

      $o->maybe(undef); # Ok, valid value $o->tolerant(undef); # Dies, invalid value

      I presume this is intentional, so it's useless to me.