3

This works:

use Moops;
class Foo :ro {
    use Types::Common::Numeric qw(PositiveOrZeroInt);
    has from => required => true, isa => PositiveOrZeroInt;
    has to => required => true, isa => PositiveOrZeroInt, trigger => method($to) {
        die 'must be from ≤ to' unless $self->from <= $to
    };
}
Foo->new(from => 0, to => 1); # ok
Foo->new(from => 1, to => 0); # "must be from ≤ to at …"

I would like to be able to make the constraint part of the type, somehow.

use Moops;
class Bar :ro {
    use Types::Common::Numeric qw(PositiveOrZeroInt);
    has from => required => true, isa => PositiveOrZeroInt;
    has to => required => true, isa => PositiveOrZeroInt->where(sub {
        $self->from <= $_
    });
}
Bar->new(from => 0, to => 1);
# Global symbol "$self" requires explicit package name
# (did you forget to declare "my $self"?)

I checked that the where sub only receives one parameter.

daxim
  • 39,270
  • 4
  • 65
  • 132
  • In standard Moose, you'd do that in `BUILDARGS`, right? Your working example relies on `to` being set before `from` is set, so it _might_ not always work. I think the attributes are sort of individual entities and can't know about their friends. A constraint about another thing by definition can't be part of that entity. I don't think putting that constraint in the type is the correct approach. Or at least, I'm having trouble wrapping my head around it. – simbabque Nov 05 '20 at 12:22
  • 1
    `BUILDARGS` is quite similar to `trigger`, except this runs before the attribute type checks, so I'd have to type checks them twice in order to guard the numeric comparison operator from blowing up on garbage input, which is lame. I can rely on both attributes already existing in a trigger because at that point in time the object has already finished constructing, so accessors work as normal. You seem to be misremembering the order being relevant here, that's the case for an attribute's [`default` option](http://p3rl.org/Class::MOP::Attribute#default) only. – daxim Nov 05 '20 at 12:46

1 Answers1

2

If you wanted to do it in a type check, you could combine the two attributes into one attribute which would be an arrayref holding both numbers.

use Moops;

class Foo :ro {
    use Types::Standard qw(Tuple);
    use Types::Common::Numeric qw(PositiveOrZeroInt);
    
    has from_and_to => (
        required => true,
        isa      => Tuple->of(PositiveOrZeroInt, PositiveOrZeroInt)->where(sub {
            $_->[0] <= $_->[1];
        }),

        # Provide `from` and `to` methods to fetch values
        handles_via => 'Array',
        handles => {
            'from' => [ get => 0 ],
            'to'   => [ get => 1 ],
        },
    );
    
    # Allow `from` and `to` to be separate in the constructor
    method BUILDARGS {
        my %args = ( @_==1 ? %{$_[0]} : @_ );
        $args{from_and_to} ||= [ $args{from}, $args{to} ];
        \%args;
    }
}

Foo->new(from => 0, to => 1); # ok
Foo->new(from => 1, to => 0); # "must be from ≤ to at …"

I wouldn't do it in a type check though. I'd do it in BUILD (no, not BUILDARGS) if the attribute were read-only, or trigger if it were read-write.

tobyink
  • 13,478
  • 1
  • 23
  • 35