Skip to content

UNIVERSAL::Cor

Ovid edited this page Sep 14, 2021 · 8 revisions

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.

UNIVERSAL::Corinna

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);

UNIVERSAL::Corinna

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
}

to_string()

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.

clone(%args)

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.

object_id()

A GUID for an object. Cloning an object generates a different GUID.

meta()

Returns a MOP instance for this class.

dump()

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.

No Backwards Compatibility

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.

UNIVERSAL::Corinna methods

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!

Destructors

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.

Constructors

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.

MOP

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.