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

Is there a better way, other than the hack below, to cleanly track inheritance directly, as opposed to using SUPER via indirect syntax or statically?

When first learning about Perl packages and modules, we are told "don't do that" whenever the topic of exporting procedures from a supposed object module arises.

There are exceptions, however. Whether these are still considered a bad idea is another question; the fact is there are examples of such behavior and we have to deal with it. Witness the nifty Time::Piece module. How would you cleanly subclass this piece of work?

When I say "cleanly" subclass, what I mean is: Time::Piece is interesting, because it grafts object oriented functionality onto existing procedures, gmtime() and localtime(). In order to do so, the module resorts to some tricks. These procedures are drop-in replacements, so they must appear to behave just like the old ones, to wit: The procedures provided by Time::Piece ultimately call the _mktime() factory method. Here is the code for the critters in question:

sub gmtime { my $time = shift; $time = time if (!defined $time); _mktime($time, 0); } sub _mktime { my ($time, $islocal) = @_; if (ref($time)) { $time->[c_epoch] = undef; return wantarray ? @$time : bless [@$time, $islocal], 'Time::Piece'; } my @time = $islocal ? CORE::localtime($time) : CORE::gmtime($time); wantarray ? @time : bless [@time, $time, $islocal], 'Time::Piece'; }

There are two things here that bother me right of the bat. First, most importantly, the main constructor for this module is presented as a private method. In order to provide "interesting" subclasses, we will no doubt be wanting to perform additional tasks during construction. We have to ignore this privacy indicator and override _mktime(). Second, the module name 'Time::Piece' is hard coded into the blessings here, rather than deriving the class name from either a ref statement or static invocation. (As a maintenance aside, I would use __PACKAGE__ here rather than hard coding the class name, but ideally class name would be derived as just described). Real problems arise once we realise that _mktime() is expected to act both as a method and as a procedure.

In order to handle seconds since the epoch as an argument, this factory method assumes such is the case when it encounters a non-reference scalar as an argument. The gmtime() and localtime implementations assume this as well, so _mktime() is invoked procedurally. This means that we are forced to provide our own implementations for gmtime() and localtime() to ensure our overridden factory method is invoked.

When you wish to override _mktime(), we are limited to calls from blessed object references. Static calls to SUPER using indirect syntax no longer work:

Time::MyPiece->SUPER::_mktime()

As you may recall, this static invocation means that the scalar string 'Time::MyPiece' is now passed as the first argument to the method...er, procedure. This string is not the number of seconds since the epoch, though it is how it will be treated.

What to do? I can think of two avenues. First, I could pester the author of Time::Piece to patch the module in such a way as to make it more amicable to subclassing. See below for at least one tweak in that direction.

Meanwhile, I'll have to make do with current reality. I need a way of invoking SUPER functionality on a procedural method -- some way of "inheriting" a non-OO piece of code, without hard coding the name of the parent class. One option is to directly traverse @ISA, though I fear it might land me in Perl Programmer Hell:

package Time::MyPiece; use base 'Time::Piece'; my $SUPER; foreach (@ISA) { $SUPER = $_ and last if $_->can('_mktime'); } ... sub _mktime { my $t; eval "$t = $SUPER::_mktime(@_)"; }

One drawback to this approach is that it assumes the parent class has also come up with a way to handle this situation in cases where it inherited the method in question.

Can anyone come up with a clean way to do this?

Meanwhile, what to do with Time::Piece? Is a fundamental redesign the answer, or would a tweak such as the following suffice:

sub _mktime { my ($time, $islocal) = @_; my $class; if (ref $time) { $class = ref $time; } elsif ($time =~ /^\D+$/) { $class = $time; $time = $class->new(@_); } if (ref($time)) { $time->[c_epoch] = undef; return wantarray ? @$time : bless [@$time, $islocal], $class; } my @time = $islocal ? CORE::localtime($time) : CORE::gmtime($time); wantarray ? @time : bless [@time, $time, $islocal], $class; }

I feel I am certainly missing something obvious. Feel free to pontificate on alternate approaches and solutions.

Matt

Replies are listed 'Best First'.
Re: Tracking Inheritance Directly due to Hybrid Methods
by djantzen (Priest) on Jul 25, 2002 at 20:37 UTC

    I don't have this module installed on my machine, so I can't test this, but what's wrong with calling the public constructor for Time::Piece from within the constructor of your subclass (thereby providing an instance to _mktime), and then reblessing the reference returned into your current package? You can do as much further initialization at that point as you want to without overriding the "private" method _mktime. It's worth noting as well that unless the documentation explicitly guards against it, you're perfectly free to interpret this as a "protected" method, which as a subclass your module may legitimately override.

    Also, as long as the module containing the procedures (same as "functions"?) you need is in your ISA array, you won't need to do any manual traversal; this is automatically done in a leftmost, depth-first search of base classes when the method/function is not found in the current module. Now, granted, if they're functions then they'll expect to be called on a class name, not on a reference to a class instance, so your callers would need to call ref($yoursubclass)->somesub(), unless you intend to override all of these public methods as in

    sub somesub { my ($this, $args) = @_; ref($this)->SUPER::somesub($args); }
    But that would be a lot of redundant code. Perhaps I'm not following this part of your discussion.

      what's wrong with calling the public constructor for Time::Piece from within the constructor of your subclass (thereby providing an instance to _mktime), and then reblessing the reference returned into your current package?
      I'd love to, but it's not that simple.
      1. It is not the only route of construction -- new objects are created either by new(), overloaded operators, gmtime(), or localtime(). The last two are procedural in nature and will not work if called statically via package name indirection (Time::MyPiece->gmtime()) since they expect either a reference or scalar seconds since epoch as first args. All of these eventually call _mktime() either as a method or procedure, so it seemed to make since to target the real constructor rather than each individual route.
      2. Even when we do target _mktime(), the blessings are hard coded as class 'Time::Piece'; we can rebless in a constructor, but we're stuck again with having to override every method that eventually calls _mktime() so that we can rebless. Not zesty.
      It's worth noting as well that unless the documentation explicitly guards against it, you're perfectly free to interpret this as a "protected" method, which as a subclass your module may legitimately override.
      Good point, I feel better about that then.
      Now, granted, if they're functions then they'll expect to be called on a class name, not on a reference to a class instance, ...
      This still passes the class name as a first argument -- as I mentioned, if the first argument is present and not a reference, then it expects it to be a scalar count of seconds since epoch, not a string representing the calling class name.

      Matt

        Okay, I think this is sinking in; gmtime and localtime are exported to your current module in such a manner that they can be called directly, that is, not on any package name or reference. Doesn't this mean then that even if you override _mktime you still have to override the exported functions so that they'll call your version of _mktime rather than the version in the package in which they were originally defined? I think you'd have to actually redefine it rather than override it, as in:

        no warnings 'redefine'; package Time::Piece; sub _mktime { ... }
        or
        sub Time::Piece::_mktime { ... }

        You know, the more I think about this, the more I think Time::Piece was written this way because it's not supposed to be subclassed ; ) Wouldn't using it as a mix-in for your modules give you all the flexibility you need? For what reason does the functionality you're looking for require an IS-A relationship to Time::Piece?

Re: Tracking Inheritance Directly due to Hybrid Methods
by mojotoad (Monsignor) on Jul 25, 2002 at 20:12 UTC
    That last _gmtime() implementation should read:

    sub _mktime { my ($time, $islocal) = @_; my $class; if (ref $time) { $class = ref $time; $time->[c_epoch] = undef; return wantarray ? @$time : bless [@$time, $islocal], $class; } if ($time !~ /^\d+$/) { $class = $time; $time = undef; } else { $class = __PACKAGE__; } my @time = $islocal ? CORE::localtime($time) : CORE::gmtime($time); wantarray ? @time : bless [@time, $time, $islocal], $class; }

    Matt