develooper Front page | perl.perl6.language | Postings from October 2006

Re: Abstract roles, classes and objects

Thread Previous | Thread Next
From:
Trey Harris
Date:
October 4, 2006 00:13
Subject:
Re: Abstract roles, classes and objects
Message ID:
20061002140046.M46238@bowser.eecs.harvard.edu
In a message dated Sun, 1 Oct 2006, Aaron Sherman writes:
> Trey Harris wrote:
>> In a message dated Fri, 29 Sep 2006, Aaron Sherman writes:
> [snip]
>>> However, that's not to say that a class can't be abstract, just that a 
>>> class that does an interface (a role with nothing but abstract methods) 
>>> must implement the role's interface.
>
>> So why would it generate an error?  Why wouldn't it merely result in B 
>> being abstract too, assuming that contra my prior mail, classes can be 
>> abstract?
>
> What use is an interface if it doesn't give you a guarantee? If I say, "all 
> dogs can bark," and you define a dog that can't bark, that's not "abstract", 
> that's a failure to meet the interface requirements of a dog.

No--I wouldn't define a dog that can't bark, I'd define a dog that *can* 
bark, but I wouldn't say *how* it could bark (conjecturing an "is 
abstract" class trait I give a sample implementation of below):

   class Dog is abstract { method bark () { ... } #[ ... ]  }
   class Pug is Dog { method bark () { self.vocalize($.barkNoise) } }

   my Dog $fido .= new; # exception, instantiating abstract class
   my Dog $fido  = Pug.new; # good, $fido can bark

Seems like there would be three ways to achieve the guarantee: an explicit 
"is abstract" marker like above to prevent instantiation; some way to 
infer concreteness (this gets into my earlier question of whether 
yada-yada is enough to do so) so you can prevent instantiation; or simply 
disallow abstract classes entirely.

It sounds like the assumption thus far has been that the existance of 
roles imply that abstract classes are disallowed, so you'd write:

   role Dog { method bark { ... } #[ ... ] }
   class Pug does Dog { method bark { .vocalize($.barkNoise) } }

S12 says: "Classes are primarily for instance management, not code reuse.
Consider using C<roles> when you simply want to factor out
common code."

But if I want to write code that handles the instance management for 
several related classes but cannot be instantiated itself, that must be a 
role and not a class.  But instance management, as the S12 quote says, is 
the purpose of classes, not roles.  Is it a deep requirement of the 
MOP/MRP that only one of classes and roles can be abstract?

I've looked at my uses of uninstantiable classes in Perl 5 for the past 
few years, and I think that almost all of them could be done as roles 
(though for some of them the cognitive dissonance of calling a set of 
object-management methods a "role" still bothers me).  But there's one 
case where I can't figure out how to do it except for throwing a runtime 
exception in a class.

For a system monitoring application, I have a class heirarchy like the 
following (bare names indicate concrete instantiable classes, brackets 
indicate uninstantiable abstract ones):

+ [SystemMonitor]
   -   CPUMonitor
   -   DiskMonitor
   +   ScriptedMonitor
       +   [HardwareMonitor]
           -   FanMonitor
           -   TempMonitor
           -   PowerSupplyMonitor

Here, SystemMonitor is abstract and sets up the data collection and 
storage routines.  Its concrete subclasses implement how to actually get 
the data and any munging the data requires, but otherwise inherit their 
behavior from SystemMonitor.  ScriptedMonitor is a concrete class that 
gets a script attribute which it runs and a closure attribute it uses to 
munge the data the script generates.

Turns out that there are many HardwareMonitors that all run the same suite 
of hardware monitoring scripts and performs the same munging on them, but 
has almost the same behavior as ScriptedMonitor.  So I handled that by 
subclassing it with a new abstract class, HardwareMonitor, which factored 
out the new behavior all the hardware monitors shared. I then subclassed 
*that* with concrete classes implementing the last little unfactorable 
bits.  So Abstract <- Concrete <- Abstract <- Concrete.

new(), for instance, was defined only in SystemMonitor (but threw an 
exception if you tried to call it on SystemMonitor, thus making the class 
abstract); gatherData() is called in SystemMonitor but is defined only in 
the direct subclasses of SystemMonitor, and is overridden in 
HardwareMonitor with a call to the superclass method 
(ScriptedMonitor::gatherData).  HardwareMonitor's subclasses just define 
some munging methods that HardwareMonitor's processData() methods calls.

In this way, I never repeat myself, I use polymorphism so that I never 
write any conditionals involving the type of anything (except in the case 
of new() throwing an exception to prevent instantiation of an abstract 
class), and related code goes together.  This is a good thing.

In Perl 6, the abstract SystemMonitor could be a role, and a concrete 
ScriptedMonitor could be a class that does SystemMonitor, but it's not at 
all clear to me what HardwareMonitor would be, since classes can't be 
abstract and roles can't inherit from classes.  I guess it would be a 
role, but then we'd have something like:

- role SystemMonitor
- class CPUMonitor does SystemMonitor
- class DiskMonitor does SystemMonitor
- class ScriptedMonitor does SystemMonitor
- role HardwareMonitor does SystemMonitor
- class FanMonitor does HardwareMonitor
- class TempMonitor does HardwareMonitor
- class PowerSupplyMonitor does HardwareMonitor

and I'd have to repeat the non-overridden parts of ScriptedMonitor in 
HardwareMonitor.

I don't see where this is a win for me--it looks very much like a loss, as 
my clear class heirarchy with no repetition has now become a flat set of 
classes with tightly-coupled mixins requiring a repetition.

I could factor out the repetition with a third role that both the 
ScriptedMonitor class and the HardwareMonitor role both do, but now I have 
one more package than I had in the Perl 5 program, and I'm not sure why. 
What have I gained by having it?

And the fact that I can't easily figure out a name that this role would 
have apart from ScriptedMonitor also alarms me--I generally consider an 
inability to see an obvious name for a software component to be a good 
sign that my software composition is faulty.

So let me just ask the question bluntly: why can't we have classes that 
can't be instantiated (short of checking .WHAT and explicitly throwing a 
runtime exception)?  Heck, to hack it in as a runtime exception, all we 
need is something like:

   role abstract {
       has @!classes;
       multi sub trait_auxiliary:is(abstract $trait, Class $container:) {
           # $container might be concrete class, so we use ::?CLASS
           # to get at the lexical class 'is abstract' was written on;
           # we keep a list because there might be multiple abstract
           # classes in our instance's composition
           @!classes.push(::?CLASS);
       }
       submethod BUILD {
           if (self.WHAT == any(@!classes)) {
               croak "Attempt to instantiate abstract class {self.WHAT}";
           }
       }
   }

Trey

Thread Previous | Thread Next


nntp.perl.org: Perl Programming lists via nntp and http.
Comments to Ask Bjørn Hansen at ask@perl.org | Group listing | About