Skip to content

Instantly share code, notes, and snippets.

@Ovid
Last active September 12, 2021 08:02
Show Gist options
  • Save Ovid/68b33259cb81c01f9a51612c7a294ede to your computer and use it in GitHub Desktop.
Save Ovid/68b33259cb81c01f9a51612c7a294ede to your computer and use it in GitHub Desktop.
Cor—A minimal object system for the Perl core

NAME

Cor — A minimal OO proposal for the Perl core

VERSION

This is version 0.10 of this document.

AUTHOR

Curtis "Ovid" Poe

CAVEAT!

Nothing in the following proposal is set in stone.

DESCRIPTION

It has been repeatedly proposed that we have OO in the Perl 5 core. I support this notion. However, there's been much disagreement over what that OO should look like. I propose a simple OO syntax that would nonetheless be modern, but still "feel like Perl 5." Here's a small taste (will be shown again later in the document):

class Cache::LRU {
    use Hash::Ordered;
    
    has cache    => ( default => method { Hash::Ordered->new } );
    has max_size => ( default => method { 20 } );

    method set ( $key, $value ) {
        if ( self->cache->exists($key) ) {
            self->cache->delete($key);
        }
        elsif ( self->cache->keys > self->max_size ) {
            self->cache->shift;
        }
        self->cache->set( $key, $value );
    }

    method get ($key) { self->cache->get($key) }
}

To distinguish this OO system from the (too) many others, such as Moose, Moo, Dios, Class::InsideOut, Mu, Spiffy, Class::Simple, Rubyish::Class, Class::Easy, Class::Tiny, Class::Std, and so on, I'm going to call this one "Cor" (short for "Corinna", a possibly fictional woman that the poet Ovid would write poems to). Using the name "Cor" is only for disambiguation. I hope Cor would become core and thus not need a name.

This document should be considered a "rough draft". While I (Ovid) am the initial author, this document has been heavily updated via feedback from Sawyer X and Stevan Little (and a bit from Matt Trout and Peter Mottram). Also, many of the underlying ideas have been directly "liberated" from Stevan's work.

Also, note that the intent is that this will ultimately be implemented in perl, not Perl. Thus, it would be written in C and likely be much faster than current options.

ASSUMPTIONS

In creating this proposal, I assumed the following:

  • No Implementation

    This document describes a possible OO system. It does not contain information about implementation. Further, general OO "details" about how roles work, how inheritance works, and so on, are mostly omitted.

  • Feature Compatibility

    We should not take away anything core Perl 5 supports. Thus, multiple-inheritance must be supported. I considered dropping it and saying "no, you have to use single inheritance", but we have a host of popular modules, such as Catalyst and DBIx::Class, which use MI and could thus not be easily ported were the authors ever inclined to do so.

  • Simplicity

    I strove to make this proposed syntax as simple as possible. This makes implementation easier and will have fewer grounds for objections.

  • Roles Must Be Included

    Most modern Perl 5 developers who use Moose/Moo use roles. Many of them use roles heavily. Thus, they will need to be supported.

  • Lexical Scope

    If possible, if the changes suggested can only apply to a given lexical scope, I suspect it will be easier to use the new classes with old code.

  • use v5.3X;

    This would be implemented as a feature and automatically be enabled if you use use v5.3X (or similar syntax). This would avoid having to jump through special hoops to use the new OO syntax. It would simply be there.

  • Safety

    Cor roles and classes assume strict and warnings by default. They also use subroutine signatures.

  • Hash References

    Assumes we use blessed hash references for the first pass. This may be revisited in the future.

  • Role Implementation

    Role implementation assumes Traits: The Formal Model (pdf) rather than the less formal Traits: Composable Units of Behavior (pdf) that is usually cited. The authors are the same, but the "Formal Model" is explicit about several assumptions made in the better-known paper.

TRIAL GRAMMAR

Below is a minimal and almost certainly incorrect grammar as a starting point for discussion.

(*
    cheating by allowing regexes and character classes
*)

Cor         ::= CLASS | ROLE
CLASS       ::=  DESCRIPTOR? 'class' NAMESPACE VERSION? DECLARATION BLOCK?
DESCRIPTOR  ::= 'abstract'
ROLE        ::= ‘role’ NAMESPACE VERSION? DECLARATION BLOCK?
NAMESPACE   ::= IDENTIFIER { '::' IDENTIFIER } VERSION?
DECLARATION ::= { PARENTS | ROLES } | { ROLES | PARENTS }
PARENTS     ::= 'isa' NAMESPACE  { ',' NAMESPACE }
ROLES       ::= 'does' NAMESPACE { ',' NAMESPACE }
IDENTIFIER  ::= [:alpha:] {[:alnum:]}
VERSION     ::= 'v' DIGIT '.' DIGIT {DIGIT}
DIGIT       ::= [0-9]
BLOCK       ::= # Work in progress. Described below

THE PROPOSAL

The bulk of this is to simply provide two things, classes and roles. The Cor syntax is deliberately simple and would be familiar to Perl 5/6 programmers, as well as programmers of other languages (single quotes imply exact text):

DESCRIPTOR 'class' NAMESPACE VERSION? DECLARATION BLOCK?
'role' NAMESPACE VERSION? DECLARATION BLOCK?
  • DESCRIPTOR

    Optional. Currently, if present, must be the keyword abstract which indicates a class that cannot be instantiated and must be subclassed.

  • 'class' or 'role'.

    One of class or role, indicating the type of this code. Required.

  • NAMESPACE

    The name (package) of the class or role. Follows current naming rules. Required.

  • VERSION

    A v-string identifying the version of this class/role. our $VERSION = inside of the BLOCK is also still allowed. Optional.

  • DECLARATION

    This will be described later, but essentially allows us to declare what classes, if any, we inherit from, and what roles, if any, we consume.

  • BLOCK

    The block of code defining the body of the class or role.

Only the 'class'/'role' and NAMESPACE and required:

class Person;
    ...
role Comparable;
    ....

If the BLOCK is not supplied the changes are file-scoped. Otherwise, they are block-scoped:

class Person     { ... }
role  Comparable { ... }

If possible, any other syntax changes suggested by this proposal would only apply to the scope of the BLOCK or file and be an error outside of the block.

Classes

In Perl 5, classes and packages are the same thing. While this has some drawbacks, it's worked reasonably well and we'll stick with this.

Basic Syntax

Cor introduces a new, simplified syntax:

class Dog v0.1 {
    method speak () { return 'Woof!' }
}

my $dog = Dog->new;
say $dog->speak;  # prints 'Woof'

Note there is no trailing semicolon required.

Alternatively, if no arguments are required, we can omit the parens with the method keyword:

class Dog v0.1 {
    method speak { return 'Woof!' }
}

Inheritance

Declaring inheritance is done via the isa keyword and takes a comma-separated list of class names (whitespace allowed). Some restrictions:

  • Cor

    You may only inherit from Cor classes as we cannot guarantee the behavior of non-Cor classes. This restriction may be removed in the future. However, for now we would prefer to maintain this restriction to avoid the possibility that Cor and non-Cor classes might need a different UNIVERSAL base class, thus altering their behavior.

  • C3

    C3 method resolution order is assumed.

      # cannot be instantiated
      abstract class Animal {
          # forward declarations are abstract methods
          # must method body must be defined by the time its called
          method speak;
      }
    
      class Dog isa Animal {
          method speak () { return 'Woof!' }
      }
    

In the above, Dog inherits from Animal.

By using whitespace to separate the classname from the version, we can also specify versions we require:

abstract class Animal v1.9 {
    method speak;
}

class Dog isa Animal v2.0 {
    method speak { return 'Woof!' }
}

The above should work according to current Perl 5 semantics (principle of least surprise).

We can also do:

class Kill::Me::Now isa I, Despise, Multiple::Inheritance { ... }

In the above, the class Kill::Me::Now inherits from I, Despise, and Multiple::Inheritance, in that order.

Role Consumption

Classes consume roles with the does keyword:

class My::Worker does Serializable, Runnable {
    ...
}

Of course, you can combine this with inheritance:

# obviously, if My::Worker consumes these roles, we do not need to repeat
# this here. This is only an example
class My::Worker::Fast isa My::Worker does Serializable, Runnable {
    ...
}

You may specify the does before the is:

class My::Worker::Fast does Serializable, Runnable isa My::Worker {
    ...
}

We don't envision supporting excluding or renaming role methods at the start, but please see the "Future Work" section.

Methods

Methods are accessed via the method keyword. Object slots (see below) are accessed via the self keyword. Methods use signatures, but a method with no arguments (aside from the invocant) may omit the signature:

method speak { say "Woof!" }

method allowed_to_vote (@people) {
    my @voters;
    foreach my $person (@people) {
        push @voters => $person 
          if self->is_on_voter_role($person);
    }
    return @voters;
}

Further:

class Foo {
    use List::Util 'sum';
    ...
    method dimsum() { ... }
}
Foo->new->sum;

The above would issue a runtime error similar to Can't find method 'sum' because the dispatcher would recognize sum as a subroutine, not a method. Further, roles would provide methods, not subroutines. This approach should eliminate the need for namespace::autoclean and friends.

Method dispatch would be resolved via the invocant class and the method name. The arguments to the method will not be considered.

Slots

Note: "slots" are internal data for the object. They provide no public API. By not defining standard is => 'ro', is => 'rw', etc., we avoid the trap of making it natural to expose everything. Instead, just a little extra work is needed by the developer to wrap slots with methods, thereby providing an affordance to keep the public interface smaller (which is generally accepted as good OO practice).

has SLOT    OPTIONS_KV;
has [SLOTS] OPTIONS_KV;

The basic slot declaration is simple;

has 'name';

By default, all slots are read-only and required to be passed to the constructor. Thus, to create an immutable point object:

class Point {
    has [ 'x', 'y' ];
    
    method to_string {
        # self->x and self->y are not available outside of this class
        return sprintf "[%d, %d]" => self->x, self->y;
    }
}

my $point = Point->new( x => 3, y => 7 );
say $point->x;            # fatal error
say $point->to_string;    # [3, 7]
Point->new( x => 4 ); # exception thrown because y is required

To provide a default:

has days_ago => ( default => method {20} );

Also, per conversation with MST, it's possible that all default slots should be automatically lazy.

Alternatively, if the default is a string, it's a method name to call (makes it easier to subclass):

has _dbh => ( default => '_build_dbh' );
method () _build_dbh { ... }

We should separate default and builder, yes? Anything with a builder would not be passed to the constructor.

Lazy slots (requires default):

has days_ago => (
    default  => method { ... },
    lazy     => 1,
);

We may wish to make the has function extensible, so that people can experiment with isa to manage their own types.

Exposing slot data requires writing a method:

class Point {
    has [qw/x y/];

    method x {self->x}
    method y {self->y}
}

Summary of slot options:

  • default => CodRef|Str (provide a default value if one is not supplied)
  • lazy => Bool (if default is provided, don't call it until it's asked for, Default is true?)
  • weaken => Bool (weaken the reference in the slot. Default is false)
  • optional => Bool (is this required by the constructor? Default is false) (unsure about this one)
  • rw => Bool (read-write, but only via the self keyword. Default is false)

Class Construction

new()

The new method would be in UNIVERSAL::Cor and should not be overridden in a subclass, though that will be allowed.. It would take an even-sized list of key/value pairs, omitting the need for a hashref:

Object->new( this => 1, that => 2 );      # good
Object->new({ this => 1, that => 2 });    # bad

BUILD

A BUILD method, just like Moose/Moo's BUILD method, will allow for additional customization:

method BUILD (%args) {
    unless ( self->verbose xor self->silent ) {
        # Speculative. We don't address exceptions in this proposal
        self->throw("You must specify one and only one of 'verbose' or 'silent' ");
    }
}

BUILDARGS

The BUILDARGS method is how Moose/Moo messes around with arguments to allow us to do things like write this:

Point->new( $x, $y );

Instead of this:

Point->new( x => $x, y => $y );

However, the BUILDARGS method has always been a bit clumsy. We don't yet have a proposal for this.

UNIVERSAL::Cor

In order to not paint ourself into a Corner (hey, I'm a papa. I can tell bad papa jokes), we should have a separate object base class which all Cor objects implicitly inherit from. At minimum:

abstract class UNIVERSAL::Cor v.01 {
    method new(%args)   { ... }
    method can ()       { ... }
    method does ()      { ... }
    method isa ()       { ... }
}

That mirrors the UNIVERSAL class we currently inherit from, but there's room for more:

abstract class UNIVERSAL::Cor v.01 {
    method new(%args)   { ... }
    method can ()       { ... }
    method does ()      { ... }
    method isa ()       { ... }

    # these new methods are merely being mentioned, not
    # suggested. All can be overridden
    method to_string ()    { ... }    # overloaded?
    method clone ()        { ... }    # (shallow?)
    method object_id ()    { ... }
    method meta ()         { ... }
    method equals ()       { ... }
    method dump ()         { ... }
    method throw($message) { ... }
}

This is still an open discussion. We don't want to pack too much into the API and cause developers pain, but there are so many "common" use cases for objects that we're tired of rewriting ad nauseum that, like many other programming languages, it might be reasonable to put them into the base class.

Opinions welcome. However, this is such a core (no pun intended) needs that understanding if we need a separate UNIVERSAL class for Cor should be decided before Cor is ready for prime time (see also, Stevan Little's UNIVERSAL::Object).

This, incidentally, is why Cor classes cannot inherit from non-Cor classes and vice-versa.

EXAMPLE

Before

Here's a simple LRU cache in Moose:

package Cache::LRU {
    use Moose;
    use Hash::Ordered;
    use namespace::autoclean;

    has '_cache' => (
        isa     => 'Hash::Ordered',
        default => sub { Hash::Ordered->new },
    );

    has 'max_size' => (
        default => 20,
    );

    sub set {
        my ( $self, $key, $value ) = @_;
        if ( $self->_cache->exists($key) ) {
            $self->_cache->delete($key);
        }
        elsif ( $self->_cache->keys >= $self->max_size ) {
            $self->_cache->shift;
        }
        $self->_cache->set( $key, $value );
    }

    sub get {
        my ( $self, $key ) = @_;
        $self->_cache->get($key)
    }

    __PACKAGE__->meta->make_immutable;
}

After

Here it is in Cor:

class Cache::LRU {
    use Hash::Ordered;
    has cache    => ( default => method { Hash::Ordered->new } );
    has max_size => ( default => method { 20 } );

    method set ( $key, $value ) {
        if ( self->cache->exists($key) ) {
            self->cache->delete($key);
        }
        elsif ( self->cache->keys > self->max_size ) {
            self->cache->shift;
        }
        self->cache->set( $key, $value );
    }

    method get ($key) { self->cache->get($key) }
}

Note that in the Cor version, any attempt do directly access the cache or max_size slots from outside the class via direct access is an error, though you can override them:

my $cache        = Cache::LRU->new( max_size => 100 );
my $hash_ordered = $cache->cache;    # fatal error

Roles

Basic Syntax

Role syntax is also simple and clear:

role Whiny {
    method whine($message) { ... }
}

class My::Class does Whiny {
    ...
}
My::Class->new->whine('some message');

Roles both provide and require methods. Any methods fully defined in the role body will be composed into the consuming class.

Any methods defined via a forward declaration are "required" to be provided by the consuming class or another role consumed by the same class.

role MyRole {
    method this;                 # class must provide this
    method that;                 # class must provide this
    method foo ($bar) { ... }    # this is provided
}

Like classes, roles may also have slots in the same matter as classes and those slots will be provided to the class. (What happens if the class defines that slot in a different way from the role? For example, if the role slot is read-write but the class slot is read-only, bugs are awaitin').

Of course, roles can consume other roles:

 role SomeRole does ThisRole, ThatRole { ... }

Strictly adhering to the concept that roles are guaranteed to be both commutative (the order of application doesn't matter) and associative (for a given set of roles, it doesn't matter which consumes what, so long as the final set is the same), the above is equivalent to:

 role ThatRole does SomeRole, ThisRole { ... }

And their aggregate behavior will be the same if one or more is consumes other roles and are in turn consumed by a class:

role ThatRole   does ThisRole { ... }
role SomeRole   does ThatRole { ... }
class SomeClass does SomeRole { ... }   # role-provided behaviors are identical

MOP

The MOP module is likely sufficient for our needs.

TODO

For the first pass, we need:

Clarify Syntax

Is the current proposed syntax acceptable? I argue that it is because I feel that it still feels "Perlish", while also feeling clean enough that developers from other languages will feel right at home.

Questions

Read-only slots with no values

This is problematic:

has 'x';

The above is a private, read-only slot with no value. Unless we require it to be passed to the constructor, it's useless and should possibly be an error. This is where our BUILDARGS work (or replacement) might come in handy. It would be good if behaviors are specified declaratively, rather than procedurally.

Class-Level Behaviors

How do we handle class data and methods?

Some argue that class data is a code smell. Fine: argue that all you want. Multiple inheritance is also a code smell, but that doesn't mean we can tell Perl developers "no." But the semantics of that can get tricky.

Class methods, however, are important. For example, you may very well want a factory class with an interface like this:

my $message = Message::Factory->create(@message_list);

Internally:

static method create (@list) {
    if ( 1 == @list ) {
        return Message::String->new( message => $list[0] );
    }
    else {
        return Message::Collection->new( messages => \@list );
    }
}

Inside that method, any attempt to call a method on the C keyword would be a syntax error. It could internally call other static (class) methods directly (?) without an invocant.

"Why only blessed hashes?"

For simplicity. We currently don't have a clear vision of how non-hashrefs can be done transparently with this. Falling back to core OO may be a solution for some. For those who know they need a blessed regex, they'll (hopefully) know enough about core OO to go ahead and run with scissors.

"Why don't we have method modifiers?"

Cor tries to be as small as possible to avoid overreach. That means "no modifiers" at this time. However, they cause an issue for roles.

Let's say a method returns the number 10. One role modifies that number by adding a 20% VAT, making the result 12. Another role modifies that to offer a discount of 3, making the result 9. However, if the discount is applied and then the VAT is added, the result is 8.4. Thus, a developer could sort the list of consumed roles and change the behavior.

In the original traits research, one of the issues they were trying to work around was the fact that inheritance order could change code behavior. Consumption of roles, however, were guaranteed to be both commutative (the order of application doesn't matter) and associative (for a given set of roles, it doesn't matter which consumes what, so long as the final set is the same). Method modifiers break this guarantee.

Future Work

I believe the initial core of Cor should be as simple as we can possibly make it to avoid too much up front work and possibly making mistakes that we cannot walk back. However, any good design of something that is both long-lasting and that we know will grow should at least be aware of future considerations. Otherwise, we might make it harder to address issues.

Types

This will wait until the core OO is there. Making has extensible might help.

Parameterized Roles

I have no suggested syntax for this, but they're extremely useful.

Role Exclusion and Renaming

Matt Trout argues, and I agree, that excluding role methods or renaming them is a code smell. However, if you don't have control over the role source code (downloading from the CPAN or being supplied by another software team), you don't always have the luxury of refactoring the code. Thus, we need to support excluding methods and renaming them.

The syntax for this is less clear at this time, but I envision something like this:

class My::Worker isa Some::Parent::Class
 does Serializable(
    excludes => ['some_method'],
    renames  => { old_method => 'new_method' } ) {

    method some_method { ... }

    method old_method (@args) { ... }
        # do something with @args
        return self->new_method(@args);
    }
}

Excluding or renaming a method automatically makes it a "required" method. This is because, even if you don't use them in your class, the role might use them internally.

This raises an issue. In the original Smalltalk traits papers, they made it clear that a role is defined by its name and the methods it supplies (methods are defined by their signature, not just the name). It's possible that someone might do this:

if ( $object->DOES('Serializable') ) {
    ...
}

At this point, we don't know if the $object class excluded any methods from the Serializable role. Thus, we don't know if any methods we expect from Serializable will conform to expectations. Thus, the naïve Does('Serializable') check may be wrong because merely having the role name isn't enough to know if the class exhibits the desired behavior.

In proper OO, the replacement methods should be semantically identical, even if they're doing different things. In reality, we know that these guarantees are often tossed out the window. I don't know that this is really a serious issue because I haven't been hit with this, but I also know that safety in building large scalable systems suggests avoiding pitfalls.

I do not have a recommendation for this, but I point it out so people can be aware of the background.

Runtime Role Application

I have no suggested syntax at this time, but this generally involves reblessing an object into an anonymous subclass which consumes the role or roles. Naturally, it's harder to guarantee object behavior, especially if several roles are applied at runtime in separate statements.

ACKNOWLEDGEMENTS

Stevan Little has been working on an object system for Perl for years. And given his background—including creating Moose and Moxie—and his constant research into a "better" way to write OO, he laid much of the groundwork for Cor.

I had been working on a pure-Perl implementation of Cor (because clearly we don't have enough object modules on the CPAN) and discussing it with the Pumpking, Sawyer, at the 2019 EU Perl Conference in Riga, when he said he wanted a spec, not an implementation.

And he's right: with P5P, there are plenty of implementors, but there's been no agreement about what should be implemented. So, working with Stevan and Sawyer, I've had to suffer the humiliation of them laughing at my amateurish mistakes, but it's made this document better as a result.

Any mistakes, of course, are mine.

UPDATE

Putting updates here so they can be easily spotted without consulting the history.

self as a keyword

What does this do?

method foo {
    some_external_function(self);
}

We can add a check on self to ensure that private slots cannot be accessed unless we're in a class or a subclass of ref self, but seems clumsy. Or we can tell developers "don't do that", but we all know what that means.

If we remove self, we need an easy way for the class to access its internal state. Lexical variables have also been proposed:

class Foo {
    has $x => ( rw => 1 );
    
    method bar ($new_x) {
        $x = $new_x;
    }
}

Feels unperlish to me, but hey, what do I know? :)

BUILDARGS

We're trying to figure out a better syntax.

@leonerd
Copy link

leonerd commented Oct 29, 2019

Indeed. This new syntax would only be meaningful inside a method function, and so it's really no great problem to just say that you can't interpolate certain weird forms of $. in there. It might have a tiny mental overlap for being slightly tricky to move code between those, you can't just copy-and-paste entirely without thinking. But then you couldn't do that from method to sub anyway because of no slot visibility.

In other thoughts: $:foo is just as clashy technically, but since it relates to formats ("The current set of characters after which a string may be broken to fill continuation fields in a format") I think even fewer people are likely to care about being able to interpolate it in modern code.

@notbenh
Copy link

notbenh commented Oct 29, 2019

I really like where this is going, thanks @Ovid, and everyone else involved. I'm most curious around how strict the version idea will be enforced. I just skimmed the comments so it's possible that this has been discussed and I missed it. It's also very possible that I'm making too much of the idea of version semantics proposed.

Given the way that the proposal is written, leveraging of version definitions, it implies that it will be more than just a helpful notation. Thus, is there an expectation that there will be a semantic way to restrict to known working cases? For example Role v2 can only be consumed by Class::A v3+ or Class::B v7+. Is the current thinking that this will be something left to the consumer to write on there own or are there going to be some notion of a semantic way to define this?

Also, on the topic of MI, is the expectation that multiple versions of Classes and Roles will live next to each other? If so then how would a specific version be requested? I don't even know how I would write it so here's a guess to illustrate but I hate this syntax:

my $Obj_1 = Some::Class[v1]->new(...); 
my $Obj_2 = Some::Class[v2]->new(...);

Lastly, if versions are indeed being promoted to actual leverageable meta data then I'm a little confused about the example for
class Dog isa Animal v2.0 { as this could be Dog of no version is a child of Animal v2.0 but it could also be read as Dog v2.0 is a child of Animal no version. Also what would it look like if I want to define a version of Dog that is a child of a specific version of Animal? Is that something currently being pondered?

@Grinnz
Copy link

Grinnz commented Oct 29, 2019

Also, on the topic of MI, is the expectation that multiple versions of Classes and Roles will live next to each other?

As long as a class or role is still a package name, this is not possible. The usual case and what I assume is being demonstrated here is a minimum version requirement, no more or less.

@nrdvana
Copy link

nrdvana commented Oct 30, 2019

@notbenh I would assume that class Foo v0.1 { ... } is shorthand for

package Foo;
our $VERSION= v0.1;

so just a shorthand for declaring the version. Since @Grinnz and I came to different conclusions, @Ovid should probably clarify that.

And on that note, this feature would require tooling updates because there are a lot of package analyzers and Dist::Zilla plugins that try munging the code based on variations of our $VERSION syntax and would need to be told how to interact with the new syntax.

@matthewpersico
Copy link

Seeing as it has been quiet, and that the conversation has veered off into the technical woods, I'd like to throw in this observation:

I am currently writing Python at my job and I really abhor the syntax for associative arrays. I keep thinking to myself: "If I was using Perl, I'd just use a hash with a sub hash with two elements and I'd just set $buildobs{$source} = { dpack_count => $dcount, binaries = {} } and then set each binary later as I get it with $buildobs{$source}->{binaries}->{$binaryName} = 1 and..." yada yada yada.

But then I start using said structure later on and boy the code is just littered with arrows and %{} derefs and all that noise Perl haters love to hate.

Wouldn't it have been easier to just create a class where I could have getters and setters? I could make the 'binaries' a 'UniqueSet' instead of faking it with a hash of values of 1. I could make cleaner looking method calls instead of line noise direct access.

I think that as we move along here, we should make sure that one criteria for the new classes is that the basic syntax for definition should be so simple that you would eventually create classes instead of naked hashes.

Just a thought.

@Grinnz
Copy link

Grinnz commented Jan 1, 2020

Idle thoughts about the still unresolved "should private slots be accessible to subclasses" question: By default say no, so that either now or in the future a modifier can be specified by the user to allow such access (in which case the subclass would allow references to any such-declared slots from its inheritance hierarchy). I believe this is referred to as a "protected" attribute in Java, and it is useful at times.

@Ovid
Copy link
Author

Ovid commented Jan 2, 2020

@Grinnz I agree that private slots should not be available to subclasses, but we protected is probably something we need. For example, in the canonical Point2D example, if you don't expose the x and y values directly to the outside world, you still want a way for $point1->distance_to($point2) to work.

@cxw42
Copy link

cxw42 commented Jan 2, 2020

Unrelated — re. the isa keyword — I see in perldelta 5.31.7 that isa is being added as an operator. Will that require a syntax change in Cor? Edit I see in the original proposal that this is intended to be implemented in C, not as a .pm, so presumably the Perl grammar would distinguish operator-isa from inheritance-isa.

@Ovid
Copy link
Author

Ovid commented Jan 2, 2020

@cxw42 Thanks for that information. I wasn't aware of it.

@Grinnz
Copy link

Grinnz commented Jan 7, 2020

@cxw42 it will be enabled by a feature flag. The isa component of this grammar can easily take precedence over such an operator.

@boftx
Copy link

boftx commented Feb 24, 2020

What is wrong with just putting Moo into Perl 5 core? We just finished fighting over Perl 5/Per; 6 for over a decade!

@Ovid
Copy link
Author

Ovid commented Feb 24, 2020

@boftx

Curiously, no one is fighting over the idea of Cor. People accept we need modern OO in the Perl core, but the disputes are about syntax issues, not whether or not it should be done.

And why? The short answer: because it's embarrassing for Perl to not have modern OO. And Moose/Moo, despite being far better than core Perl, are not modern OO.

  • Moo doesn't have a metaclass
  • Moo/se makes it hard to handle cases where we need private data
  • Moo/se encourages exposing all of your data, even if you don't want to
  • It's still slower than creating a core Perl object
  • Our inability to distinguish between methods and subroutines causes ugly internal hacks that most don't notice
  • I can make debugging and profiling a nightmare due to all of the extra classes involved
  • It hobbles us as we try to modernize the language (wouldn't it be nice to not have the SUPER:: bug or indirect method syntax?)

And as we're learning more and more, we're discovering that as great as Moose/Moo are, they're broken and they have designs which naturally encourage OO programmers to build fragile code.

And let's talk about the broken has function that gets exported. It tries to handle:

  • Data
  • Attributes
  • Types
  • Coercion
  • Delegation
  • Clearers
  • Predicates
  • Documentation
  • Constructor args
  • Default values
  • Overriding
  • … and more!

It's no wonder that sometimes Moo/Moose gets painful when trying to do the right thing.

So let's consider a case where we have private data that we absolutely don't want an accessor for, or to have it be set via the constructor. Here's one way to do that:

# declaring the attribute
has seKret => (
    is       => 'bare', # no accessor
    isa      => 'Str',
    init_arg => undef, # can't set it in constructor
    lazy     => 1,
    builder  => '_build_seKret',
);

Yeah, that's painful. Here's a Cor implementation of that:

has Str $seKret :builder;

One of those is much easier to understand (and teach). In other words, Cor makes it easy to do what you want.

Oh, and how do you fetch that secret value in Moose? (Can't really do that in Moo because you have no metaclass (and yes, I know how Moo inflates to Moose)):

# fetching its value
my $name = $self->meta
                ->get_attribute('seKret')
                ->get_value($self);

No one's going to do that. Instead, they'll just declare an attribute with a leading underscore and hope for the best.

Or what if we discover a method that's very, very expensive to call, but only needs to have its value calculated once? Why not convert it to an attribute? Well, doing that correctly is painful:

has expensive_data => (
    is       => 'ro',
    isa      => 'Num',
    init_arg => undef,
    lazy     => 1,
    builder  => '_build_expensive_data',
);

In Cor (though we're still nailing down the syntax):

has Num $expensive_data :ro :no-constructor :builder;

In short, Cor is designed to make it easy to do the right thing.

Moose/Moo are designed to be easy, but offer terrible affordances for doing the wrong thing, and while they're better than core Perl OO, they're still terrible in many ways.

@dk
Copy link

dk commented Feb 24, 2020

@Ovid is there a IRC shannel or mailing list or something else for discussion? Gist comment thread doesn't seem to be fitting quite well for that.

Otherwise a great idea I think! Just please make 'has foo isa => 'Hash', default => sub { {} }' into 'has foo => default => {}' :)

@tobyink
Copy link

tobyink commented Feb 24, 2020

use Zydeco factory_package => 'MyApp';

class Cache::LRU {
  require Hash::Ordered;
  has $cache   = Hash::Ordered->new;
  has max_size = 20;
  
  method set (Str $key, Any $value) {
    if ( $self->$cache->exists($key) ) {
      $self->$cache->delete($key);
    }
    elsif ( $self->$cache->keys > $self->max_size ) {
      $self->$cache->shift;
    }
    $self->$cache->set( $key, $value );
  }
  
  method get (Str $key) {
    $self->$cache->get($key);
  }
}

my $cache = Cache::LRU->new(max_size => 100);
$cache->set('foo' => 123);
say $cache->get('foo');

(Above is actual working code. Zydeco is on CPAN.)

@cxw42
Copy link

cxw42 commented Feb 24, 2020

@dk Agreed!

@Ovid Might it be time to move this draft into a repo with an .md file we can send PRs to and open issues against? That seems to be working reasonably well for JS TC39 drafts, and the Raku team used that process for the rename discussion (Path to Raku). Perhaps Perl/core-oo?

@Ovid
Copy link
Author

Ovid commented Feb 24, 2020

@tobyink: I really have been enjoying what I see happening with Zydeco and I wish it was more widespread! Sadly, it doesn't meet Cor's needs.

  • Minimal changes possible to allow it to be accepted into core
  • Attributes should default to "no accessor"
  • Objects should default to immutable

Plus, Zydeco commits the same sins as Moo/se in regard to heavily, heavily overloading the has function.

@tobyink
Copy link

tobyink commented Feb 24, 2020

@Ovid: the $cache accessor in that example is lexical, so the accessor cannot be called from outside the class block. (It's even stored inside out.) And I have lately been rethinking whether defaulting to is=>rw was a sensible decision. Personally I prefer is=>ro as a default, and there are good arguments in favour of is=>bare. I might change the default at some point soon. My thought process on defaulting to is=>rw was "yes, read only is better, but if you've got read-write, nobody's forcing you to write to it".

@boftx
Copy link

boftx commented Feb 25, 2020 via email

@dk
Copy link

dk commented Feb 25, 2020

About default ro, I’d like to disagree. It always seemed counter-intuitive that normal perl variables are rw‘s, while Moose has it as ro. For me it was thus rather annoying to set rw explicitly. I’d rather prefer default rw

@tobyink
Copy link

tobyink commented Feb 25, 2020

@dk, Moose has no default at all.

$ perl -e'use Moose; has q/foo/'
Attribute (foo) of class main has no associated methods (did you mean to provide an "is" argument?)

But "ro" makes a lot more sense to use with object attributes in most cases. The main reason for this is that objects are assigned by reference instead of by copying data. My LPW 2014 presentation is related.
http://buzzword.org.uk/2014/lpw-presentation/three-weird-tricks.pdf

@dk
Copy link

dk commented Feb 25, 2020

@tobyink, yes, I stand corrected on Moose default - but that doesn't change the idea I'm addressing that it's not rw by default. Having said that I cannot say that I agree with the "stop creating mutable objects" message. At most, there are models that are best served by mutable, and there are models that are best served by immutable objects. In my experience the former is much more widespread in the perl world.

@notbenh
Copy link

notbenh commented Feb 25, 2020

@dk you are right that there's a set of assumed defaults that work better in given environments. I guess I would be intrigued to know what the usecase is for an attribute accessor that behaves more like a global variable than a static memoized value?

My common pattern is to have an object that represents persisted values (think db and the like). Thus having an ro default accessor with the builder to do the look up. Then in the case of an update that's just an actual method that will do the actual data store update and invalidate the memoization of the accessor (ie clearer), still gives me the one-time look up cost for a value while still allowing an easy 'write' from the view of the caller. While you can still have the same persistance in the case of an rw attribute, it just happens via way of a trigger so the "actual data store save" is done after rather than before, so there's some possible syncing issues that may arise assuming that code assumes that value is what's stored.

@notbenh
Copy link

notbenh commented Feb 25, 2020

@tobyink I'm a bit confused and this might be a contextual problem as I've only looked at the slides of your three tricks talk.

In the case of the pony name, given that example it looks like you are sharing one pony with two people, so I would completely expect that updating $alice's pony's name would be completely reflected by the call to get_name on $bob's pony. That said I do completely agree that the use of rename is far more obvious to what's going on. But that seems like a label problem rather than a pattern problem? Am I missing context?

The start date is the case of the expectation is to use a value rather than a reference, and this is not a problem that really has anything special to do with OO as the same problem exists in any case where you have the option of a reference. Again not sure if there's more nuance that I've missed. [EDIT: this usecase may highlight that there may be a good case for Cor to have some kind of 'copy the value' system that is better than digging thru the MOP]

Lastly you make a valid point about having a system that encourages documentation for public facing code... but then suggest use of builders as undocumented but subclass-able public facing code. So not sure if I'm just reading more in to that ultimatum that was intended?

All that said I agree completely that what ever code gets finalized it should encourage solid patterns. As I stated above I'm a huge fan of the lazy ro attribute with builder and clearer's as they afford me both the benefit of what appears to be a static well named value that is accessable. The ablity to subclass builders affords a huge level of flexibility while maintaining a level of clear expectations. But I guess for me an object is just a handy representation of what can be done with some idea of state and that state can't be static forever /shrug.

@dk
Copy link

dk commented Feb 25, 2020

@notbenh my common patterns are graphical-reliated stuff, rectangles, widgets, pens etc. All of these have inherently mutable methods. As for DB access I don't think it applies well as an argument if I guess correctly that you define table fields automatically, not manually DBIx::Class-style. In automatic declarations one is not burdened to write lots of redundant 'is => ro'.

Another argument is what would happen if one is negligent with the declaration and forgets 'is' altogether, ending with a wrong access type: For f.ex. gui one will get a read-only exception, while in DB table record data will be just discarded without a fatal effect.

@autarch
Copy link

autarch commented Mar 21, 2020

One question I had on reading this latest draft is whether there have been any thoughts on MOP extensions, particularly extensions that change the behavior or roles or classes.

IMO, one of the best things about Moose is that the entirety of its behavior in terms of code generation is written in Perl and can be extended (and the worst part is that writing these extensions requires deep digging into the Moose internals and much black magic). This has allowed for a rich ecosystem of MooseX extension distributions on CPAN, and it's freed up the Moose core from having to be everything for everyone.

Without this sort of system, I'd expect this new core OO to constantly come up short for many people's needs.

Here's some suggestions for things that I think this new system should support somehow:

  • MooseX::StrictConstructor - this really should be built in, and a non-strict constructor that ignores extra params could probably be done with the as-yet-undefined BUILDARGS method. But how would I ship that as an extension on CPAN? Could I ship something that subclasses UNIVERSAL::Cor and then people can subclass my class? That would work, but then how many such parent classes will people have to use in some cases?
  • MooseX-Getopt - this actually does relatively little metamagic, but that little bit is key to how it functions.
  • Various accessor generators - you know folks will want this!
  • MooseX::Clone - if this isn't implemented in the UNIVERSAL::Cor package it should be possible to do on CPAN.
  • The native attributes stuff that is part of the Moose core originally lived on CPAN as an extension. I don't think Cor has to provide this feature, but again, it should be possible for me to implement and ship to CPAN.

My big concern is that this will be implemented in a way that either makes extensions impossible or requires you to write XS code to create an extension. This should be part of the spec. The MOP distro on CPAN is not at all sufficient for this, since it's just a read-only view of your class.

I would note that at least for Getopt and Clone, all you need is a way to add arbitrary metadata to slots, but with Moose you also get typo detection, so if you write traits => ['Getopts'] you will get an error telling you that no such metaclass exists. You still don't get typo detection on the additional metadata key names, so you can write cmd_flags and this will be silently ignored.

@cxw42
Copy link

cxw42 commented Mar 23, 2020

@Ovid I see https://github.com/Ovid/Cor is open - thanks! Am I correct that all discussion should move to that repo?

@Ovid
Copy link
Author

Ovid commented Mar 23, 2020

@cxw42 Yes, that's correct. In particular, the wiki has a proposals section and every proposal read for review has a link to a github issue for comments.

@yuki-kimoto
Copy link

I write an idea. This is not using twigil.

class Point {
  has x;
  has y;

  method foo () {
    $self->{x}++;
    $self->{x} = 1;
    my $x = $self->{x};

    my $cb = sub { $self->{x}++ };

    print "($self->{x}, $self->{y})";
  }
}

If internal data structure is not hash reference, $self->{x} is become synonym.

For example, If internal data structure is array ref [4, 5], $self->{x} means $self->[0].

@druud
Copy link

druud commented Sep 10, 2021 via email

@Ovid
Copy link
Author

Ovid commented Sep 12, 2021

To all responding, note that this gist is almost two years old. The modern work on this project is at https://github.com/Ovid/Cor

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment