-
Notifications
You must be signed in to change notification settings - Fork 19
UNIVERSAL::Cor
Please see the main page of the repo for the actual RFC. As it states there:
Anything in the Wiki should be considered "rough drafts."
Click here to provide feedback.
All classes in Corinna inherit implicitly from UNIVERSAL::Corinna
. This class, in turn, inherits from UNIVERSAL
. This is to ensure Corinna OO does not break non-Corinna OO.
abstract class UNIVERSAL::Corinna isa UNIVERSAL {...}
But this might mean that I can't inherit from non-Corinna classes. If I want a Corinna class to inherit from HTML::TokeParser::Simple to provide a better interface, I can't, but I can fake it via delegation:
has $file :new :isa(FileName);
has $parser :handles(get_token, get_tag, peek) = HTML::TokeParser::Simple->new($file);
This is a first-pass suggestion of the Corinna object behavior. It does provide a lot of behavior, but hopefully with sensible defaults that are easy to override. In particular, it might be nice to have the to_string
be automatically called when the object is stringified. No more manual string overloading!
abstract class UNIVERSAL::Corinna v0.01 {
method new(%args) { ... }
method can (@method_names) { ... } # Returns true if invocant provides all listed methods
method does (@role_names) { ... } # Returns true if invocant consumes all listed roles
method isa (@class_names) { ... } # Returns true if invocant inherits all listed classes
# these might be better fetched from the MOP
method methods () { ... } # Returns a list of all methods provided by invocant
method roles () { ... } # Returns a list of all roles consumed by invocant
method parents () { ... } # Returns a list of all classes inherited by invocant
# these new methods are merely being mentioned, not
# suggested. All can be overridden
method to_string () { ... } # overloaded?
method clone (%kv) { ... } # shallow
method object_id () { ... } # guid, but only for the life of the program?
method meta () { ... } # MOP
method dump () { ... } # read-only to show internals (for debugging)
# these are "phases" and not really methods. They're like `BEGIN`, `CHECK`
# and friends, but for classes
CONSTRUCT { ... } # similar to Moose's BUILDARGS
NEW { ... } # object construction
ADJUST { ... } # similar to Moose's BUILD
DESTRUCT { ... } # similar to Moose's DEMOLISH
}
Overloaded stringification of Corinna classes is assumed and the to_string
method will be called automatically. It behaves like the current object stringification, but is easy to override in a subclass.
Provide a shallow clone of the object. It takes an optional list of named arguments, which it then uses to update the values of any corresponding slots.
For example (lifted from a Damian Conway example):
class Box {
has ($height, $width, $depth) :reader :new :isa(PositiveNum);
...
}
my $original_box = Box->new(height=>1, width=>2, depth=>3);
my $cloned_box = $original_box->clone(); # h=1, w=2, d=3
my $updated_box = $original_box->clone(depth=>9); # h=1, w=2, d=9
The argument list permitted by clone() should be identical to that permitted by new(), and should be processed in exactly the same way, with the caveat that required arguments, of course, can be supplied by the existing object.
Also, we strongly discourage constructors with positional arguments for any but the most trivial cases. You can't read Box->new(4,5,6)
and know what those variables mean.
A GUID for an object. Cloning an object generates a different GUID.
Returns a MOP instance for this class.
Provides a string dump of data slots, perhaps similar to Data::Printer. Used only for debugging.
Poorly named. Suggestions needed.
For CONSTRUCT
, ADJUST
, NEW
, or DESTRUCT
phases, see Constructors and Destructors.
Note: we want to have backwards compatibility, but for the first pass, there are issues with it and will complicate our work.
Every time I've tried to construct a scenario where we can safely inherit from non-Corinna classes, things are difficult. If you inherit from one, your base class is now UNIVERSAL
, not UNIVERSAL::Corinna
and everything blows up.
Or you can inherit from a non-Corinna class and maybe have Perl walk the inheritance chain until it finds all parent classes inheriting from UNIVERSAL
and swap that with UNIVERSAL::Corinna
. But then those "non-Cor" classes just might provide methods which override the UNIVERSAL::Corinna
methods and all sorts of lovely breakage occurs. Inheritance is a mess (which is why Dr. Alan Kay, the inventor of the term "Object Oriented", doesn't really consider inheritance to be a core thing needed for OO).
So I thought we could do this:
abstract class UNIVERSAL::Corinna isa UNIVERSAL does UNIVERSAL::Role::Corinna {...}
And put most of the behavior in UNIVERSAL::Role::Cor
and inheriting from a non-Corinna class becomes this:
class HTML::TokeParser::Corinna isa HTML::TokeParser::Simple does UNIVERSAL::Role::Corinna { ... }
But that causes other issues. For example, you can't easily override those methods in your class because they're flattened into your class. Or you provide your own methods and you can no longer call the needed parent methods because they're gone.
So, um, maybe have a mixin?
class HTML::TokeParser::Corinna isa HTML::TokeParser::Simple {
...
}
Corinna sees that the class we're inheriting from does not inherit from UNIVERSA::Cor
, so it creates an anonymous class that is inserted between the Corinna and core class (this is how Ruby mixins work):
+--------------------------+
| HTML::TokeParser::Simple |
+--------------------------+
^
|
+--------------------------+
| Cor::Mixin::123456 |
+--------------------------+
^
|
+--------------------------+
| HTML::TokeParser::Corinna |
+--------------------------+
It is the Cor::Mixin::
class that would consume the UNIVERSAL::Role::Cor
role, thus allowing HMTL::TokeParser::Cor
to override the universal behavior, but allow HTML::TokeParser::Simple
to not inherit what it doesn't need.
And that means we have a curious situation where the universal abstract base class is inheriting from HTML::TokeParser::Simple
and if that doesn't look like a big bucket of bugs waiting to happen, you've never programmed before.
But, people (understandably) want back-compat, so we have that silent "anonymous" class sitting between the Corinna and non-Corinna classes, but that means we have methods the non-Corinna classes don't know about and when it calls the ->parents
method and gets something back which is radically different from what it expected, BOOM!
Or we take all of those methods out and shove them safely into our meta object, but now if the Cor class wants to override them, it has to subclass the metaclass to provide new behavior and override the meta
method to return the new subclass. So you have two classes for every class you want to write! What a damned headache!
But let's look at destructors. Let's assume your Corinna code has a DESTRUCT
phase. The non-Corinna code might or might not have a DESTROY
method. You don't know if it does and the whole point of OO is encapsulation (or isolation) and having to know internal details of an object is not a good thing. Further, even if the parent class does or does not have a destructor, there's no guarantee that this won't change in the future because its existence is generally not part of the class contract.
So let's look at this:
package Parent {
use strict;
use warnings;
sub new { bless {...} => shift }
... more code
sub DESTROY {...}
}
class Child isa Parent {
... code and attributes
sub DESTRUCT {...}
}
When your $child
gets destroyed, Corinna now has to track completely separate kinds of destructors and probably know to call DESTRUCT
before DESTROY
. It used to be that Perl simply called DESTROY
if it was present and that's that (and remember that DESTROY
overrides the parent method and you need to call the parent manually if you need to, another delightful encapsulation violation). But in Cor, it would call DESTRUCT
and walk up the inheritance tree to call all other DESTRUCT
methods, creating a destruction object to pass to each one. The Corinna needs to know about the old type of OO and hunt for a DESTROY
method and call that.
This means that putting any sort of cleanup behavior in UNIVERSAL::Corinna::DESTRUCT
would be a ticking timebomb because if that really did handle all of the cleanup itself, it could possibly render the invocant unsafe before we can call the parent DESTROY
methods. I've looked at various ways of dealing with this and all of them kind of suck.
If Corinna inherits from non-Cor, what happens with the parent constructor call? Does Corinna know to call the parent new
method? It should, but what does it get back? A blessed hash, probably. Maybe something else. Corinna ultimately would like to move away from blessed hashes, but it's not clear what that would mean if we're already using them all over the place.
Or consider the heuristics of trying to fetch a list of methods from the MOP. You can't tell what is a method or not in Perl because they're all subroutines that, by happenstance, assume the invocant is their first argument. So inheriting from non-Corinna means that your MOP quite possibly becomes needlessly complex with heuristics to deal with this very common case. Probably more time would be spent fixing those heuristics for non-Corinna than in developing Cor.
Corinna—Bringing Modern OO to Perl