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

Dear monks,

I have a whole bunch of different objects; for example "node" objects, which can go into a "tree" object, which can go into a "trees" (note: plural) object. The underlying data structures for the "tree" and "trees" objects are array refs. Likewise, I also have "datum" objects, which go into a "matrix" object, which go into a "matrices" object. Again, the "matrix" and "matrices" objects are blessed anonymous array refs.

Now, all objects that are blessed array refs inherit from the abstract "listable" object, which has a bunch of methods that are handy for array-like structures (for example filters, where you loop through all entities in the array and return all entities that meet a certain condition). I also want to be able to have a listable method "insert", with the following behaviour:
$tree->insert($node); #works $matrix->insert($datum); #works $trees->insert($node); #error: $trees object can't contain $node $matrix->insert($node); #error: $matrix object can't contain $node
Also, I want this Listable::insert method to have the following properties: ...so that the method also works for "specialnode" objects that inherit from the "node" object (for example).

Here's what I did to achieve this: all objects have two methods that return strings to indicate i) what "container_type" the object is; ii) what type of "container" it can be inserted into. Then, internally, the Listable::insert method becomes sort of like this:
sub insert { my ( $self, $obj_to_insert ) = @_; if ( $self->container_type eq $obj_to_insert->container ) { push(@{$self}, $obj_to_insert); } else { die "I can't hold that object!"; } }
The downside of this is that every insertion involves two method calls and a string comparison. Also, it feels a bit hokey, I think.

So now my question: can anyone recommend a more elegant and efficient way to achieve the same behaviour?

Thanks!

Replies are listed 'Best First'.
Re: "polymorphism" hack: nuts or wise?
by Ovid (Cardinal) on Aug 26, 2005 at 17:37 UTC

    Well, I'll have to think about a more elegant and efficient way, but so far I don't see any serious problem with what you are doing. Remember the saying "a sufficiently encapsulated hack is no longer a hack." The questions to ask are: "is this really causing efficiency problems?" and "is there a more maintainable solution?" If it suits your needs, don't fix it without good reason. This is a Bad Idea.

    Elaborating: let's say you make this method 50% more efficient in terms of "time" (loosely speaking). If you find out that it's only taking up 1% of your programs runtime, you possibly have wasted time making a non-issue more performant. Further, I find for myself that when I start worrying about performance issues, even on systems I've built from the ground up, I almost always guess wrong about where the performance bottlenecks are (even the "obvious" database performance issues may be memory bound, IO bound or just bloody awful queries).

    That being said, I would personally create a private $can_contain method.

    my $can_contain = sub { my ( $self, $object ) = @_; $self->container_type eq $object->container; }; sub insert { my ( $self, $object ) = @_; unless ( $self->$can_contain( $object ) ) { die "I can't hold that object!"; } push @{$self}, $object; }

    With private methods like that, you can be guaranteed that no on else will inherit them, no one outside of your class will see the data they don't need to see and if anything else in the method needs to know if it "can contain" something, you're safe.

    Though I still wouldn't fix it until it merits the fix :)

    Cheers,
    Ovid

    New address of my CGI Course.

Re: "polymorphism" hack: nuts or wise?
by merlyn (Sage) on Aug 26, 2005 at 17:54 UTC
    can anyone recommend a more elegant and efficient way to achieve the same behaviour?
    You could use Class::Multimethods to create a "insert" that dispatches on the types of its first two arguments, then implement all the ones that make sense, and let an exception be thrown for combinations that fail.

    The nice thing is that this module emulates a similar strategy for Perl6, so you'll be able to reuse your experience more directly as time passes.

    -- Randal L. Schwartz, Perl hacker
    Be sure to read my standard disclaimer if this is a reply.

Re: "polymorphism" hack: nuts or wise?
by exussum0 (Vicar) on Aug 26, 2005 at 17:44 UTC
    Someone should follow up my node: If you can determine which datatype is being "inserted" into your container, by checking the inheritance tree and by multi-inheriting a dummy class dictating the class type (node vs something else), you may come up w/ a cleaner implementation. You won't have these random methods in your datatypes-to-be-inserted that will make no sense out of context of the applicaiton.

    In the end, you want a good cohesion, but loose coupling. Imagine if you took half of your classes and threw them in another project. Will those classes and their extra methods make sense elsewhere?

    This doesn't easily apply to business-level classes, since everyone's business works different. :)

    ----
    Give me strength for today.. I will not talk it away..
    Just for a moment.. It will burn through the clouds.. and shine down on me.

      Can you give a good example of how that works here? The original poster didn't say these objects were related via inheritance. A Tree probably isn't a subclass of Trees. The latter sounds like a collection of the former. The only inheritance being talked about in that node is the "listable" abstract base class. Trying to shoehorn inheritance into this model could be a bad thing, but maybe you're seeing something I'm not.

      Cheers,
      Ovid

      New address of my CGI Course.

        In this case, inheritance would just be used to mark classes as implementing a certain interface. There is no "implements" equivalent in Perl, so you can fake it by making an empty class with a descriptive name and POD that describes an interface, and then adding it to the @ISA for classes which implement that interface. Then you have a simple way to check, via isa().
        Sure. And I'm suggesting inheritance as a different road than using methods to determine type. He doesn't wish for nodes to be inserted into certain containers, no?

        Check out Re: "polymorphism" hack: nuts or wise?,(Update Re^3: "polymorphism" hack: nuts or wise?). Grape limes link a bike?

        ----
        Give me strength for today.. I will not talk it away.. Just for a moment..
        It will burn through the clouds.. and shine down on me.

        Yes, you're right: a node isn't a special case of a tree - and neither is a tree a special case of a trees object. Also, I'm a bit worried about multiple inheritance. I may want to implement use fields; and I can imagine going crazy if I have multiple inheritance.
Re: "polymorphism" hack: nuts or wise?
by Tanktalus (Canon) on Aug 26, 2005 at 17:49 UTC

    Let's break this down just a bit. First you say you want to ensure that a container can only contain certain types of things. In programming-speak, that says "comparison required". So you have two choices: string or integer comparison. Unless performance is critical, string seems fine to me.

    Now, as to how you're testing, by using the inheritance tree to call the right function, that doesn't seem too bad to me, either. Normally one would check the other way around - check if $obj_to_insert->isa($self->contained_type()), I think, but either way could work, depending on the design goals.

Re: "polymorphism" hack: nuts or wise?
by dragonchild (Archbishop) on Aug 27, 2005 at 02:32 UTC
    I'm surprised and actually kind of disturbed that no-one's suggested this. Why not just ask the object to make that determination for you?
    sub Listable::insert { my ($self, $child) = @_; die "Cannot insert ", ref($child), " into a ", ref($self) unless $self->is_legal_to_insert($child); push @{$self}, $child; } sub Listable::insert { return 1; } sub Matrices::is_legal_to_insert { my ($self, $child) = @_; return 1 if $child->isa( 'Matrix' ); return $self->SUPER::is_legal_to_insert( $child ); }

    Each class gets a bite at the apple to say if such'n'such is legal or not to insert. Now, I chose to make a definition based on the fact that I'm assuming "Matrices" are only allowed to contain items of type "Matrix". You could say "Foo" is allowed to have everything except "Bar", in which case that would look like:

    sub Foo::is_legal_to_insert { my ($self, $child) = @_; return 0 if $child->isa( 'Bar' ); return $self->SUPER::is_legal_to_insert( $child ); }

    Now, in this solution, you will always want to redispatch to your parent if you don't have an opinion on the matter. This is because you never know if today's parent will be your parent or your grandparent tomorrow. This way, you're refactor-proof.

    Now, we're asking the container if it's allowed to hold children of that class. Another option would be to ask the children if they're allowed to be held by that type of container. That may not make sense for your current project, but it's an option that you should be conceptually familiar with.


    My criteria for good software:
    1. Does it work?
    2. Can someone else come in, make a change, and be reasonably certain no bugs were introduced?
      Intuitively, I would say it is "better" design for the children to be asked who can hold them rather than the other way around. My sense is that this is related to the notion that child objects should know about the parent object they inherit from rather than the other way around. Not sure if this is that important, or applicable, in this particular case, though.
Re: "polymorphism" hack: nuts or wise?
by sgifford (Prior) on Aug 26, 2005 at 17:52 UTC
    It seems to me you've just changed the nature of the problem slightly: before, if you created a new specialnode that inherits from node, you had to modify all of the container objects to know about the new type. Now, if you create a new container type specialmatrix object that inherits from matrix, you have to modify all of the containable object to know about this new type.

    One way to solve the problem is to use the isa method from UNIVERSAL. You can say if ($obj->isa('Node')) to see if an object is a Node, or inherits from Node.

      You can say if ($obj->isa('Node')) to see if an object is a Node, or inherits from Node.
      Is that even necessary? If an object inherits from Node, it'll have the Object->container method in the same way as the parent Node object has (provided, that is, if the object doesn't override the method). Also, it would send me back to a chain of if / elsif / else statements?