Skip to content

Proposed RFC

Ovid edited this page Jun 13, 2021 · 37 revisions

Leave feedback here.


Pursuant to the new RFC process Perl is experimenting with, we will create an RFC for Corinna.

This is an incomplete WIP that will be worked on as we have time. It is VERY alpha.


Preamble

Author:  Curtis "Ovid" Poe <[email protected]>
Sponsor:
ID:      OVID
Status:  Exploratory
Title:   Corinna—Bring Modern Object-Oriented Programming (OOP) to the Perl core

Abstract

It's time to bring modern OOP to the Perl core, but we still plan to keep Perl being Perl.

Motivation

Depending on how you what you call an OOP system, the CPAN appears to have 80+ contenders. It's a bewildering array of buggy, half-implemented systems. Even the best of them have limitations, largely imposed by the Perl language itself. If you've not already done so, I strongly recommend reading The Lisp Curse. That explains our mess in spades.

We're trying to solve the ever-present problem with "what OOP system do I use?" coupled with "Perl looks ancient."

Existing syntax is hurting Perl, not helping int. While we don't propose changing or removing bless, it's the "assembly language of OO." It allows you to build powerful things on top of it, but every who does is spending time repeating lots of boilerplate code, rewriting the same bugs, and implementing different OO systems with different semantics, making it harder for a developer to learn the quirks.

Of the existing CPAN modules, Moo/se seems to have won, but they have numerous issues, partly due to limitations imposed by the Perl language itself.

Rationale

The syntax of Corinna is clear, concise, and makes it easier to write safe code, along with avoiding some of the mistakes inherent in Moo/se. Further, by having a clear set of semantics up front, one developers "learn" Corinna syntax, it's the same everywhere. Java was designed to be portable across architectures. Corinna is designed to be portable across developers.

But why not another system? As we understand it, Moose has already been rejected because it would pull in a large number of modules into the Perl core and P5P does not wish to maintain them.

Moo is faster and smaller, but thanks to how the meta method works, it's easy to try metaprogramming and get your Moo class inflated to Moose. Thus, including Moo in the core would force us to either include Moose or to break backwards compatibility.

Further, Moo/se:

  • Uses blessed hashrefs and this doesn't encapsulate/isolate your data
  • The attributes make it hard to not expose them to consumers (including subclasses), making it more difficult to minimize your contract
  • It's natural to write mutable objects in Moo/se, creating reference structures with strange action at a distance
  • Moo/se encourages creating "builders" which allow subclasses to override parent class data which should be private
  • Due to legacy code, we have to allow both hashrefs and key value pairs in constructors, unnecessarily complicating the implementation and breaking poorly-implemented BUILDARGS.

There's more which can be said, but the Moo/se issues have taught us a huge amount about what we would like in OO, but come with considerable baggage. I'm sure we can easily troll through the innumerable alternatives on the CPAN and find similar issues.

Stevan Little's Moxie is of great interest, but the syntax is unfortunate and it's still tied to Perl's limitations.

Specification

The specification would be daunting for the RFC. It's largely based on our MVC description and the Object::Pad test suite.

The primary grammar looks like:

Corinna     ::= CLASS | ROLE
CLASS       ::= DESCRIPTOR? 'class' NAMESPACE
                DECLARATION BLOCK
DESCRIPTOR  ::= 'abstract'
ROLE        ::= 'role' NAMESPACE
                DECLARATION BLOCK
NAMESPACE   ::= IDENTIFIER { '::' IDENTIFIER } VERSION? 
DECLARATION ::= { PARENT | ROLES } | { ROLES | PARENT }
PARENT      ::= 'isa' NAMESPACE
ROLES       ::= 'does' NAMESPACE { ',' NAMESPACE }
IDENTIFIER  ::= [:alpha:] {[:alnum:]}
VERSION     ::= 'v' DIGIT {DIGIT} '.' DIGIT {DIGIT} '.' DIGIT {DIGIT}
DIGIT       ::= [0-9]
BLOCK       ::= # Perl +/- Extras

The method grammar (skipping some bits to avoid defining a grammar for Perl):

METHOD     ::= MODIFIERS 'method' SIGNATURE '{' (perl code) '}'
SIGNATURE  ::= METHODNAME '(' current sub argument structure + extra work from Dave Mitchell ')'
METHODNAME ::= [a-zA-Z_]\w*
MODIFIERS  ::= MODIFIER { MODIFIER }
MODIFIER   ::= 'has' | 'private' | 'overrides' | 'abstract' | 'common' 

There are a few things worth noting, First, there is only single inheritance. Code reuse of OO behavior is done via compositing roles or delegation. Corinna offers native support for delegation:

has $created :handles(...) = DateTime->now;

Backwards Compatibility

Currently, Corinna's syntax is generally backwards-compatible because the code does not parse on older Perls that use strict. This is helped tremendously by requiring a postfix block syntax which encapsulates the changes, rather than the standard class Foo is Bar; has ... syntax.

$ perl -Mstrict -Mwarnings -E 'class Foo { has $x; }'
Global symbol "$x" requires explicit package name (did you forget to declare "my $x"?) at -e line 1.
syntax error at -e line 1, near "; }"
Execution of -e aborted due to compilation errors.

Various incantations all cause the same failures. If strict is not used, you will get runtime failures with strange error messages due to indirect object syntax:

$ perl -e 'class Foo { has $x }'
Can't call method "has" on an undefined value at -e line 1.

In a pathological case, you use strict but you have an empty class or role body, you will also get errors due to indirect object syntax because Perl will think the block delimiters, { ... } are a hashref and not a block.

As for tooling, we hope that B::Deparse, Devel::Cover, and Devel::NYTProf, won't be impacted too strongly. However, this has not yet been tested.

PPI (and thus Perl::Critic and friends) will be impacted, but we have defined a regular grammar for Corinna, making parsing much easier.

Also, is it possible to emulate this for earlier Perl versions (or at least, a useful and correct subset), even if slow? And if not, what sort of API or functionality is missing that if added would make similar future "polyfill"s possible?

Security Implications

Most of what we plan leverages Perl's current capabilities, but with a different grammar. We don't anticipate particular security issues. In fact, due to increased encapsulation, Corinna might actually be a bit more secure (in terms of data it exposes).

Examples

Here is an LRU cache demonstrating many of the features of Corinna (but not roles):

use feature 'class';

class Cache::LRU v0.1.0 {
    use Hash::Ordered;
    use Carp 'croak';

    common $num_caches :reader       = 0;
    has    $cache      :handles(get) = Hash::Ordered->new;
    has    $max_size   :new  :reader = 20;
    has    $created    :reader       = time;

    ADJUST { # called after new()
        $num_caches++;
        if ( $max_size} < 1 ) {
            croak(...);
        }
    }
    DESTRUCT ($destruction) { $num_caches-- }

    method set ( $key, $value ) {
        if ( $cache->exists($key) ) {
            $cache->delete($key);
        }
        elsif ( $cache->keys > $max_size ) {
            $cache->shift;
        }
        $cache->set( $key, $value );  # new values in front
    }
}

Prototype Implementation

Paul "LeoNerd" Evans has been using Object::Pad as a test bed for many of these ideas, though he's included many things we don't intend for V1. However, we understand that Object::Pad is already stable enough that at least one company is using it in production.

FAQ

Why not Moo/se or alternatives in the core?

The MVP for Corinna is designed to be the smallest possible useful OOP that we can get into the Perl core. We don't want to do too much in the MVP, lest we get tied down with bad design decisions we can't easily walk back. The best alternatives we see now are very feature complete and thus might violate the idea of "minimum" viable concept.

P5P has (as we understand it) already rejected Moose in the core due to its huge non-core dependency list. Moo in the core would have to break backwards-compatibility with the meta method, leading to potential wide scale breakage of the darkpan.

Further, even with Moxie, we find that the implementation is limited by the syntax of the Perl language itself. There are no true methods. It's tied to a blessed hashref, making it easy to violate encapsulation (and this is needed because without public readers/writers, the instance needs to reach into the hashref to get its data). The syntax is still a bit clumsy, requiring readers/writers to be declared separately from their slots.

We actually find Zydeco and Dios interesting, but the scope of those projects is probably far larger than what could go into the core (and we haven't reviewed them thoroughly enough to sure they're appropriate).

Open Issues

The astute reader that between this and the Overview (MVP) document, there are quite a few things that are not specified. For example, while we have an extremely detailed specification for object construction, we have not defined objects, classes, or methods. Some terms are so common that repeating definitions everywhere would take months.

Other things, such as the exact nature of the destruction object that DESTRUCT takes, or whether or not DESTRUCT is even a phaser. We're torn on this.

We want the specification to be detailed enough that P5P can make a decision, but want it to be loose enough that if we need to change some aspects, we don't want P5P to feel like we've pulled a bait and switch.

So we're hoping to get approval for an iterative, agile approach. With the great feedback from so many people, we've gotten most of those beaten into a solid shape and we're comfortable with it, but no matter how careful we are, we're going to make mistakes and we don't want to go down the waterfall approach of specifying everything to the nth degree and committing ourselves to a bad design.

Copyright

Copyright (C) 2021, Curtis "Ovid" Poe

This document and code and documentation within it may be used, redistributed and/or modified under the same terms as Perl itself.

Clone this wiki locally