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

I could tell the solution I'm currently working on, and I have a few, but I really want the full breadth of what PM has to offer on this one.

I've made a testing system it works as follows:

In side the Quiz.pm I have problem code such as:
sub new_question { my $self = shift; my $question = new NHB::QuizMaster::Custom::NHMCC::Question( @_ ); push @{ $self->questions }, $question; $question; }


That code is what I'm trying to kill, see the base classes, are in NHB::QuizMaster, but a user can add plugin/custom types that extend those, (are subclasses of them) in NHB::QuizMaster::Custom, the names are assumed to be the same Quiz.pm/Answer.pm/Question.pm. How can I make it so when someone subclasses both Quiz, AND Question, they don't have to statically type a package for the Question.

Can I make a lib hack, that adds the path to the MODULE (not the origin the script ran at). Or is there an entirely better solution to set the calling chain up?

I would like it so if the user says something like new NHB::QuizMaster::Custom::NHMCCD::Quiz that this quiz which Moose extends NHB::QuizMaster::Quiz first looks in its own path NHB::QuizMaster::Custom::NHMCCD, for Question.pm, and then in the path of NHB::QuizMaster...

Thanks in advance


Evan Carroll
www.EvanCarroll.com

Replies are listed 'Best First'.
Re: Calling chains, extends, and subclassing
by stvn (Monsignor) on Oct 04, 2006 at 18:58 UTC
    EvanCarroll

    This is a common problem with parrallel class heirarchies, and one which no one has managed to solve well. It is sometimes referred to as the "Expression Problem", here is a paper which describes how it is solved in Scala using Traits. However, I don't think this particular solution would be possible using Moose.

    The Moose-ish solution would be as follows:

    package EvanCarroll::Quiz; use Moose; has 'question_class' => (is => 'ro', default => 'EvalCarroll::Question +'); sub new_question { my $self = shift; my $question = $self->question_class->new(@_); push @{ $self->questions }, $question; $question; }
    Now, at this point you can do a number of things. The first one being using the constructor parameters for Quiz like so:
    my $quiz = EvanCarroll::Quiz->new( question_class => 'My::Custom::Question' );
    From what I understand of your design, this code would go into QuizMaster::Custom, which I assume would know what custom question class to use before it created the Quiz instance.

    Of course you can also do the same in QuizMaster with the Quiz class, and get that to be dynamic as well.

    Now if you want to allow the author of a custom Quiz subclass to supply an arbitrary Question class, and not enforce namespace consistency across Quiz and Question, then your user could just do this:

    package Stvn::Quiz; use Moose; extends 'EvalCarroll::Quiz'; has '+question_class' => (default => 'Some::Random::Question');
    The '+' in front of the attribute name tells Moose to clone the attribute from it's superclass (in this case EvanCarroll::Quiz) and to change just the 'default' value for question_class to be 'Some::Random::Question'.

    And of course, this does not prevent your Quiz::Master class from still forcing the question_class parameter in the Quiz constructor (as shown above). Instead it only creates a new default for the new Quiz subclass.

    Hope this helps.

    -stvn
      Firstly, this is an awesome suggestion.


      How would suggest how to load these though, eval "require $self->question_class;"; before my $question = $self->question_class->new(@_); or is there a better way to do this too?


      Evan Carroll
      www.EvanCarroll.com
        ... is there a better way to do this too?

        Not really, UNIVERSAL::require is nice, but it pollutes UNIVERSAL, which is bad. Inside Moose we actually do this:

        my $file = $class . '.pm'; $file =~ s{::}{/}g; eval { CORE::require($file) }; confess("Could not load module '$class' because : $@") if $@;
        whenever we need to load a class. But aside from that I don't have a better suggestion.

        -stvn
Re: Calling chains, extends, and subclassing
by diotalevi (Canon) on Oct 04, 2006 at 13:13 UTC

    First, never say new SomeClass in OO code. You can usually get away with it when writing non-OO code that merely uses OO code but you're really asking for trouble when you do it in an OO module. The problem is that if you say new Whatever in some class like Question you mean Whatever->new but if your local Question->new is already defined by the time you write the above line then you really get Question->new instead. It's 100% a bug on your part and you should never do this.

    Secondly, use Ovid's aliased module to get short names. Recommend it to your users. Then they can have short names too without any extreme hacks to ISA.

    use aliased 'NHB::QuizMaster::Custom::NHMCCD::Quiz'; use aliased 'NHB::QuizMaster::Custom::NHMCCD::Question'; use aliased 'NHB::QuizMaster::Custom::NHMCCD::Answer'; # Now use the Quiz->, Answer->, Question-> namespaces.

    ⠤⠤ ⠙⠊⠕⠞⠁⠇⠑⠧⠊

      The alias deal doesn't seem to be an apt suggestion for a few reasons,
      1. It would require me to subclass all three packages, rather than just the select ones I wanted to subclass
      2. Even if I wanted to subclass the three modules, unless subclass exported and poluted the namespace of every other package (superclass), it would seem as if I would have to have totally redundant code
      So now in the subclass, I would have something like
      ##Non redundant code
      use aliased 'NHB::QuizMaster::Custom::NHMCCD::Question';
      
      ##Totally redundant code
      sub new_question {
        my $self = shift;
        my $question = new Question( @_ ); Question->new(@_);
      
        push @{ $self->questions }, $question;
      
        $question;
      }
      

      I'm guessing that the alias doesn't affect the superclass



      Evan Carroll
      www.EvanCarroll.com

        You said "new Question" which you already know is ass. Don't be an idiot, don't do that.

        aliased isn't a structural thing - it means you don't have to use long class names in your code. It doesn't solve any subclassing or other interface problems. I suggested that you trash your entire idea to do @ISA evil and get to nirvana through syntactic sugar.

        ⠤⠤ ⠙⠊⠕⠞⠁⠇⠑⠧⠊

Re: Calling chains, extends, and subclassing
by jbert (Priest) on Oct 04, 2006 at 11:23 UTC
    If I've understood correctly, you've got parallel class heirarchies, Quiz/Answer/Question. If someone does a custom subclassing of all three (or some two) you want to avoid them having to specify the full type name of the related class?

    Its a bit icky, but you could put 'make_question' into your base Quiz class, munge the type name and do something like this:

    package NHB::QuizMaster::Quiz; sub make_question { my $s = shift; my $type = ref $s; # X::Y::Z::Quiz $type =~ s/::Quiz$/::Question/; my $question = new $type; ...etc }
    I would have expected this to require no strict 'refs', but it doesn't seem to.

    It also feels a bit icky, but maybe that's just me.

Re: Calling chains, extends, and subclassing
by mreece (Friar) on Oct 04, 2006 at 17:34 UTC
    something along these lines, perhaps (untested)?
    sub new_question { my $self = shift; my $class = ref $self; ( my $question_class = $class ) =~ s{::Quiz$}{::Question}; ( my $question_module = $class ) =~ s{::}{/}g; my $question = eval { require "$question_module.pm" } ? $question_class->new( @_ ) : NHB::QuizMaster::Question->new( @_ ) ; push @{ $self->questions }, $question; $question; }