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


in reply to Moose "unions" and inheritance

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'

Replies are listed 'Best First'.
Re^2: Moose "unions" and inheritance
by remiah (Hermit) on Nov 30, 2012 at 11:22 UTC

    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 for good example and clear explanation...

        So, all methods of Person using name attribute get troubled.

        At first I thought "around" at SecretAgent will work somehow, but "name" is already overrided and it is "ArrayRef" at Person's introduce_yourself method.

        This is really caution for me.

Re^2: Moose "unions" and inheritance
by PetaMem (Priest) on Nov 30, 2012 at 11:42 UTC

    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'
        Moose type constraints are not strings, they are instances of Moose::Meta::TypeConstraint.
        Sure, I assumed they have semantics beyond 'being a String'. I just meant from the syntactic perspective, Moose::Manual::Types pretty much makes it look like the types are strings. At least
        ... isa => Str | ArrayRef, ...

        doesn't work without MooseX::Types (Bareword "Str" not allowed while "strict subs"...)

        Anyway, I tried to make this inherited union constraints "do what I mean" for multiple inheritance and came up with
        package ValAtom; use strict; use warnings; use 5.10.1; use Moose; use MooseX::Types::Moose -all; extends 'KeyAtom', 'JustTesting'; has '+data' => ( isa => __PACKAGE__->_existing_constraint('data') . ' | ArrayRe +f | HashRef', ); sub _existing_constraint { my ($class, $attr) = @_; return join ' | ', map { $_->meta->find_attribute_by_name($attr)->type_constraint } $class->meta->superclasses; } 1;
        Back to string again - obviously - as I couldn't figure out how to do it otherwise. But this seems to work.

        Bye
         PetaMem
            All Perl:   MT, NLP, NLU