-
Notifications
You must be signed in to change notification settings - Fork 19
Rationale
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.
The name "Cor" is short for "Corinna", a woman the poet Ovid wrote about in Amores. The fact that it sounds like "core" is a happy coincidence.
When Perl 5.0 was released over two decades ago, a primitive OO system was included. A simple Customer
class might look like this:
package Customer;
use strict;
use warnings;
sub new {
my ( $class, $name, $birthdate ) = @_;
return bless {
name => $name,
birthdate => $birthdate,
}, $class;
}
sub name {
my $self = shift;
if (@_) {
$self->{name} = shift;
}
return $self->{name};
}
sub birthdate {
my $self = shift;
if (@_) {
$self->{birthdate} = shift;
}
return $self->{birthdate};
}
1;
That's very ugly, requires a lot of boilerplate code, and offers absolutely no safety. If you want to assign a database handle as the customer's name, there's nothing stopping you!
Larry has stated before that the minimal OO system was a chance to build better OO systems on top of it. If we think of the "bare bones OO" as an experimental playground, it's been a rousing success with over a hundred modules on the CPAN, all trying to compete in this space. Many of them are little more than attempts to remove the boilerplate you see above.
However, if we think of the bare bones OO as a useful system, it's not a success. It's painful to write, drives away new developers, and has little to attract interest. However, over the years, a clear success story has arisen, and that's the Moo/se family of OO. They've mostly dominated OO programming in Perl and many have asked that they simply be made core. In Moose, the above code could be written as this:
package Customer;
use Moose;
has 'name' => (
is => 'rw',
isa => 'Str',
required => 1,
);
has 'birthdate' => (
is => 'rw',
isa => 'DateTime',
required => 1,
);
1;
That's much nicer and offers a bit of type safety. It is not, however, in the core. Worse, telling developers to install something from the CPAN to get decent OO then becomes a huge hurdle to just playing with the language due to them needing to come up to speed with installing CPAN modules. Moo
makes this slightly easier (much less stuff to install), but it suffers from the same issue.
Further, Moo/se has performance issues. It also conflates instance data with accessors with object construction. And we still are stuck unrolling the @_
array and I'm going to scream if I have to explain, once again, why my $foo = @_;
doesn't work!
Enter Cor
This Corinna class does what the Moose and raw Perl does:
class Customer {
has $name :isa(Str) :new :reader :writer;
has $birthdate :isa(DateTime) :new :reader :writer;
}
It has numerous advantages over core Perl and Moo/se, but let's implement a naïve LRU cache in Corinna to see what's going on.
class Cache::LRU {
use Hash::Ordered;
has $cache :handles(get) = Hash::Ordered->new;
has $max_size :new :reader = 20;
has $created :reader = time;
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
}
}
Before we dive in, you can read this core Perl implementation to compare:
package Cache::LRU;
use strict;
use warnings;
use Carp;
use Hash::Ordered;
sub new {
my ( $class, %arg_for ) = @_;
my $cache = Hash::Ordered->new;
my $max_size = $arg_for{max_size} || 20;
unless ( $max_size + 0 > 1 ) {
croak("Invalid max_size argument: $max_size");
}
bless {
cache => $cache,
max_size => $max_size,
created => time,
} => $class;
}
sub created { return $_[0]->{created} }
sub max_size { return $_[0]->{max_size} }
sub _cache { return $_[0]->{cache} }
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);
}
1;
Note: every example of a class that I've written in Corinna has been shorter and easier to read than either core Perl or Moo/se.
First, let's look at the cache declaration:
has $cache :handles(get) :builder;
method _build_cache () { Hash::Ordered->new }
has $cache
is similar to my $cache
, except that it declares an instance
variable and is only valid inside of a Corinna block.
Because there is no variant of a :new
attribute, this says "this is private
and you may not pass it via the constructor".
The :handles(get)
attribute says "if someone calls ->get
on this this
class, attempt to delegate it to whatever we have in this slot. In this case,
that's the value the :builder
method returns, in this case, Hash::Ordered->new;
. Because it's a :builder
and not a default (has ... = $something
), the value is lazy .
But what if a builder value mustn't be lazy? You can supply the :immediate
attribute to have its value calculated immediately upon object construction:
has $cache :handles(get) :builder :immediate;
method _build_cache () { Hash::Ordered->new }
But in the above case, because we have no extra logic, it's probably better to just set a default:
has $cache :handles(get) = Hash::Ordered->new;
Next we have our $created
slot.
has $created :reader = time;
In the above, we have a new :reader
attribute which says "create a read-only method name created
to read this value. But if you want a different method name, that's fine: :reader(get_created)
.
We also have a value we can optionally pass to the contructor because we have a default value:
has $max_size :new :reader :isa(PositiveInt) = 20;
We have a type, :isa(PositiveInt)
, because we don't want someone passing -3
or "foo" for this value. The others omitted types because we were going to
control them internally and I wanted to show you that you don't have to use
them if you don't want to.
So let's look at the set
method:
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
}
You don't see it in the above code, but you automatically get a $self
variable in a method if you declare it with method
. Also, because we can
access our $cache
an $max_size
instance variables directly, our code is
shorter, easier to read, and avoids the method call overhead (this promises
one of many huge performance boosts). It also means that you cannot override
these in subclasses, but this is a feature. If you want a subclass to override
your data, you must design your class to be subclassable.
The goals of Corinna are simple:
- Bring modern OO to the Perl code
- Still feel like Perl
- Focus on a small MVP
The small aspect is important. Things like parameterized roles, async, role exclusion and aliasing, and maybe even the type system itself will likely to omitted in the first pass. This is for two reasons. First, we want something small enough that it's attractive to P5P with less room for arguments. Second, if we juggle too many moving parts and we discover a terrible problem in our foundation, we might paint ourselves in a corner. So let's not do that.
It's worth noting that the expressiveness of Corinna is almost on par with that of Raku and is far, far clearer than most other languages. If done properly, Cor provides an OO model people would actually want to program in. And it may even provide us a road to a future Perl. Imagine this:
module String::Utils {
sub trim (Str $string) {...}
sub reverse (Str $string) {...}
sub crunch (Str $string) {...}
...
}
By experimenting with syntax that is illegal in older versions of Perl, we give ourselves a clean slate in modernizing Perl without breaking backwards compatibility!
It's time.
Corinna—Bringing Modern OO to Perl