Some of you might have heard me asking questions and moaning lately about subclassing Time::Piece. Plenty of good points have been raised, not the least of which is "why subclass". Is there a point at which the effort of subclassing, which should be easy in theory, is no longer worth the gains of doing so? Probably. But I trudged forward anyway.

Here I summarize a few of the problems encountered, show my "minimal" solution (least amount of code rewriting) and offer a chance for fellow monks to toss eggs at my sinful ways, or better yet, offer up improvements.

Time::Piece is a module that grafts some object oriented features to the traditional uses of localtime() and gmtime(). It is a drop-in modification, so when it is used the aforementioned functions behave just as they did before. This requirement also happens to be one of the main source of problems when it comes to subclassing.

There are four ways to get a new Time::Piece object. There is a constructor, new(), the gmtime() and localtime() functions which return Time::Piece objects in scalar context, plus there are overloaded arithmetic operators (used in conjunction with other Time::Piece objects, Time::Seconds objects, or scalar values representing seconds).

In addition to the arithmetic operators, Time::Piece objects also overload stringification so that the objects returned from gmtime() and localtime() still appear to produce a date string in scalar context.

All four of these paths to Time::Piece construction eventually utilize a private method _mktime() that performs the blessing, etc. This is what I referred to in the past as a "hybrid" method, because it operates either as an object method or as a function that takes seconds since epoch as an argument (in order to accommodate gmtime() and localtime()). What it does not currently do is act as a static method, which takes the package name as a first argument in such constructs as Time::Piece->new.

The goal is to have a subclass produce new instants of the subclass via any of the four construction methods described above -- not producing a Time::Piece object.

Overriding _mktime() does not work since it also operates as a function. It's difficult to track isa relationships on procedural methods, because a) they aren't methods, and b) Time::Piece hard codes its blessings as Time::Piece objects.

Overriding new(), gmtime(), localtime(), and all arithmetic operators is overkill akin to reimplementation -- undesirable.

After discussions here on the Monastery with brother fever, I decided that the most efficient way to approach the problem (aside from contacting the author and submitting a patch for Time::Piece) was to replace Time::Piece::_mktime() with a friendlier version. The new version accomodates both seconds since epoch or class names as arguments.

This does not solve everything, though, particularly my desire to have new objects blessed into their respective derived classes, no matter how far along the path of inheritance they reside. For that, I ended up having to leave a trail of "inheritance crumbs", sneakily using the import() method for my nefarious purposes. (for the curious that examine the code below, import() is invoked before @ISA relationships are established, hence the delay in actually invoking isa() calls).

Below you will find my results. The first generation subclass, TimeTame.pm, does most of the nasty dirty work. Notice how much easier subsequent subclassing is with TimeFoo.pm (the exports still have to be carried forward, which is right, but I wish there was some sort of "re_export" sort of pragma for Exporter). Also included is a small sample script that prints out some basic inheritance information.

Fellow Brethren. I know I have sinned in this quixotic quest. Set your codes of purity aside for the moment, however, and share with us better solutions.

Matt
TimeTame.pm
package TimeTame; # Minimal subclassing attempt for Time::Piece use 5.006; use strict; use warnings; use Carp; require Exporter; use Time::Piece; use base qw( Time::Piece Exporter ); our @EXPORT = @Time::Piece::EXPORT; our %EXPORT_TAGS = %Time::Piece::EXPORT_TAGS; # So our exported time functions and overloaded operators work on our # new derived classes (and any that derive from *this* class), we # leave a bread crumb trail during the import. We can't directly check # isa relationships at this point because @ISA has not been set up # yet. We'll delay that check for later, using our crumbs. our($Maybe_Bless, $Source_Bless); sub import { my $call = 0; my $pkg; while ($pkg = caller($call)) { last unless $pkg && $pkg ne 'main'; $Maybe_Bless = $pkg; ++$call; } $Maybe_Bless ||= __PACKAGE__; __PACKAGE__->export_to_level(1, @_); } { no warnings 'redefine'; sub Time::Piece::_mktime { my ($time, $islocal) = @_; my $class; if (ref $time) { $class = ref $time; $time->[Time::Piece::c_epoch] = undef; return wantarray ? @$time : bless [@$time, $islocal], $class; } if ($time !~ /^\d+$/) { $class = $time; $time = undef; } else { $class = $Source_Bless ? $Source_Bless : __PACKAGE__->_set_bless +; } my @time = $islocal ? CORE::localtime($time) : CORE::gmtime($time); wantarray ? @time : bless [@time, $time, $islocal], $class; } use warnings; } sub _set_bless { my $class = shift; $class ||= __PACKAGE__; $Source_Bless = $Maybe_Bless if $Maybe_Bless && $Maybe_Bless->isa($class); $Source_Bless ||= $class; } 1;
TimeFoo.pm
package TimeFoo; use 5.006; use strict; use warnings; require Exporter; use TimeTame; use base qw(TimeTame Exporter); our @EXPORT = @TimeTame::EXPORT; our %EXPORT_TAGS = %TimeTame::EXPORT_TAGS; 1;
tt.pl
#!/usr/bin/perl -w use TimeFoo; use Time::Seconds; foreach $str (qw(TimeFoo TimeTame Time::Piece)) { print "TimeFoo ", TimeFoo->isa($str) ? "isa " : "is not a ", "$str\n +"; } $t = localtime; print "localtime made a ", ref $t, " $t\n"; $t2 = TimeFoo->new; print "new made a ", ref $t2, " $t2\n"; $t3 = $t2 + ONE_DAY; print "Add made a ", ref $t3, " $t3\n";