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