http://www.perlmonks.org?node_id=1006446

PetaMem has asked for the wisdom of the Perl Monks concerning the following question:

Fellow monks,

I'm trying to refactor some legacy code using a Moose'ish way. The old code has tons of codepaths to work with different types of data (objects may be strings, hashrefs, regexps, arrayrefs) and I thought using Moose unions would probably be the way to go.

Coercions are probably not the way to go, at least not in the general case, (the code may use coercions when e.g. 'consolidating' data structures).

In order to get a little bit more structure to the whole thing, I thought of an abstraction into two Moose classes: 'KeyAtom' and 'ValAtom', where KeyAtom may be of type 'Str | RegexpRef' and ValAtom of the types that are valid for KeyAtom plus 'HashRef | ArrayRef'.

Unfortunately, doing (shortened)
package KeyAtom; has 'data' => ( is => 'rw', isa => 'Str | RegexpRef', ); package ValAtom; extends 'KeyAtom'; has 'data' => ( is => 'rw', isa => 'KeyAtom | ArrayRef | HashRef', );
Doesn't work. Of course, because KeyAtom naturally isn't the same as 'Str | RegexpRef'. So when the definition of ValAtom data type would have been.
has 'data' => ( is => 'rw', isa => 'Str | RegexpRef | ArrayRef | HashRef', );
All works. However, true happiness doesn't come up, as this requires to manually maintain basically redundant information. So the core question is how to get the type definition in the inherited classes "do what I mean"?

Bye
 PetaMem
    All Perl:   MT, NLP, NLU

Replies are listed 'Best First'.
Re: Moose "unions" and inheritance
by tobyink (Canon) on Nov 30, 2012 at 10:27 UTC

    Firstly, when overriding an attribute definition in a child class, use:

    has '+data' => ( ... );

    This allows you to inherit attribute options from the parent class, so you don't need to redeclare, say, is => 'rw' (because the parent class has already declared that).

    There isn't an especially elegant way of adding to the parent attribute's type constraints. This is the best I could do. I'm using MooseX::Types here because it makes things a little prettier. It's possible to do the same mucking around with Moose::Util::TypeConstraints but who's got the patience?!

    use v5.14; use strict; use warnings; package KeyAtom { use Moose; use MooseX::Types::Moose -all; has data => ( is => 'rw', isa => Str | RegexpRef , ); } package ValAtom { use Moose; use MooseX::Types::Moose -all; extends 'KeyAtom'; sub _existing_constraint { my ($class, $attr) = @_; return $class->meta->find_attribute_by_name($attr)->type_const +raint; } has '+data' => ( isa => __PACKAGE__->_existing_constraint('data') | ArrayRe +f | HashRef, ); } ValAtom->new(data => 'Hello'); # Str ValAtom->new(data => qr{Hello}); # RegexpRef ValAtom->new(data => []); # ArrayRef ValAtom->new(data => {}); # HashRef ValAtom->new(data => \*STDOUT); # none of the above... crash!
    perl -E'sub Monkey::do{say$_,for@_,do{($monkey=[caller(0)]->[3])=~s{::}{ }and$monkey}}"Monkey say"->Monkey::do'

      Hello tobyinc.

      So, I forgot '+' and used subtypes.

      So I read again Moose::Manual::Attributes and found statement ...

      We recommend that you exercise caution when changing the type (isa) of an inherited attribute.

      I would like to ask what is "caution" here?

        "I would like to ask what is "caution" here?"

        package Person { use Moose; has name => (is => 'ro', isa => 'Str'); sub introduce_yourself { my $self = shift; printf("My name is %s\n", $self->name); } } package SecretAgent { use Moose; extends 'Person'; # secret agents have many aliases has '+name' => (isa => 'ArrayRef'); } my $bond = SecretAgent->new( name => ['James Bond', 'Burt Saxby', 'David Somerset'], ); $bond->introduce_yourself;

        You see the problem?

        Making a type constraint tighter (e.g. if the parent class wants a Num, and the child class restricts it to an Int) should usually be just fine. Making it looser requires more caution. The author of the SecretAgent class needs to check which methods of Person assume that name is a string, and override them all.

        package SecretAgent { use Moose; extends 'Person'; # secret agents have many aliases has '+name' => (isa => 'ArrayRef'); sub introduce_yourself { my $self = shift; my @names = @{ $self->name }; my $name = $names[ rand @names ]; my $surname = (split / /, $name)[-1]; printf("The name's %s, %s\n", $surname, $name); } }
        perl -E'sub Monkey::do{say$_,for@_,do{($monkey=[caller(0)]->[3])=~s{::}{ }and$monkey}}"Monkey say"->Monkey::do'

      Thanks a lot for this example. Many lessons learned. :-)

      Starting to grok the code, I 1st wondered why the type constraints do not need to be written in a string. It seems use MooseX::Types::Moose is responsible for being able to do that.

      I wondered if it will "do the right thing" if confronted with multiple inheritance and added a "JustTesting" class like so
      package JustTesting; use strict; use warnings; use Moose; use MooseX::Types::Moose -all; has 'data' => ( is => 'ro', isa => CodeRef, );

      and then of course used extends 'KeyAtom', 'JustTesting'; in the ValAtom class definition. A subsequent my $atom4 = ValAtom->new(sub {});

      throws an error
      Attribute (data) does not pass the type constraint because: Validation + failed for 'ArrayRef|RegexpRef|Str|HashRef' with value CODE(0x25d85c +0) ...

      Indicating, that only the 1st one in the inheritance hierarchy gets propagated. So I assume for it to work independent of how many parent classes were passed, one would have to loop over them in _existing_constraint (how?) and buildup the type constraint before returning it.

      Thanks also for the +data hint, that somehow fell of my stack. Although I ask myself what happens if a inheriting class uses +data and some parents have 'rw' while others have e.g. 'ro'.

      Bye
       PetaMem
          All Perl:   MT, NLP, NLU

        "Starting to grok the code, I 1st wondered why the type constraints do not need to be written in a string. It seems use MooseX::Types::Moose is responsible for being able to do that."

        Moose type constraints are not strings, they are instances of Moose::Meta::TypeConstraint. has (and indeed the Moose::Meta::Attribute class that powers has) just gives you a little sugar allowing you to indicate type constraints as strings. But internally it translates those to type constraint objects.

        MooseX::Types basically gives you functions defined along these lines:

        # It's actually a lot more complex than this, but just # pretend it's this simple... # sub HashRef () { ### empty prototype return Moose::Meta::TypeConstraint->lookup('HashRef'); }

        So that you can use:

        isa => HashRef

        It also plays some fancy tricks overloading bitwise operators so that HashRef | ArrayRef "just works".

        So everything I did in the previous example would work without MooseX::Types; you'd just need to do a bit of work with Moose::Meta::TypeConstraint objects, so it wouldn't look as pretty.

        "Indicating, that only the 1st one in the inheritance hierarchy gets propagated. So I assume for it to work independent of how many parent classes were passed, one would have to loop over them in _existing_constraint (how?) and buildup the type constraint before returning it."

        Multiple inheritance is always going to make things messier. For example, if you're inheriting from classes A and B where:

        package A; has data => (is => 'ro'); package B; has data => (is => 'rw');

        ... what on earth would you expect to happen?!

        In cases like this, I'd want the class that's doing the multi-inheritance to define as much of the data attribute as it can, and rely on its base classes' definitions as little as possible.

        perl -E'sub Monkey::do{say$_,for@_,do{($monkey=[caller(0)]->[3])=~s{::}{ }and$monkey}}"Monkey say"->Monkey::do'
Re: Moose "unions" and inheritance
by tobyink (Canon) on Nov 30, 2012 at 12:30 UTC

    Further to my previous answer, here's another way to do it. It pushes all the ugly out to an attribute trait.

    use v5.14; use strict; use warnings; # The ugly lives in this package package MooseX::UnionInheritedTypeConstraint { use Moose::Role; use Moose::Util::TypeConstraints -all; use namespace::clean -except => ['meta']; around new => sub { my ($orig, $class, $name, %options) = @_; if (my $new = $options{isa}) { my $existing = $options{associated_class} -> find_attribute_by_name($name) -> type_constraint; if ($existing) { $new = Moose::Util::TypeConstraints::find_or_parse_typ +e_constraint($new) unless ref $new; $options{isa} = union([$existing, $new]); } } $class->_process_isa_option($name, \%options); # maybe need to process coerce too?? return $class->$orig($name, %options); }; } # No ugly below! package KeyAtom { use Moose; has data => ( is => 'rw', isa => 'Str | RegexpRef', ); } package ValAtom { use Moose; extends 'KeyAtom'; has '+data' => ( traits => [ 'MooseX::UnionInheritedTypeConstraint' ], isa => 'ArrayRef | HashRef', ); } ValAtom->new(data => 'Hello'); # Str ValAtom->new(data => qr{Hello}); # RegexpRef ValAtom->new(data => []); # ArrayRef ValAtom->new(data => {}); # HashRef ValAtom->new(data => \*STDOUT); # none of the above... crash!

    You might notice that I've managed to avoid the dependency on MooseX::Types here; though the trait should work equally well if you're using MooseX::Types.

    PS: You can actually see the code that Moose uses to check type constraints. This is sometimes handy...

    my $tc = ValAtom->meta->get_attribute('data')->type_constraint; say $tc->_inline_check('$value') if $tc->can_be_inlined;
    perl -E'sub Monkey::do{say$_,for@_,do{($monkey=[caller(0)]->[3])=~s{::}{ }and$monkey}}"Monkey say"->Monkey::do'