-
Notifications
You must be signed in to change notification settings - Fork 19
Corinna Overview
This is intended to be the "canonical" description of Corinna behavior. If other documents on the wiki disagree, this document should be considered the correct one.
This project is now referred to as "Corinna", not "Cor". Some complained that "Cor" sounds too much like "core" and, to avoid confusion, I've renamed it.
To get a sense of what the Corinna project is trying to do, here's a simple example of an LRU (least recently used) cache in Corinna, showing off some of its features.
class Cache::LRU {
use Hash::Ordered;
common $num_caches :reader = 0;
has $cache :handles(get) :builder;
has $max_size :new :reader = 20;
has $created :reader = time;
CONSTRUCT(%args) {
if ( exists $args{max_size} && $args{max_size} < 1 ) {
croak(...);
}
}
ADJUST (%args) { $num_caches++ }
DESTRUCT ($destruction) { $num_caches-- }
method _build_cache () { Hash::Ordered->new }
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
}
}
Many terms used to describe various OO systems in Perl are overloaded. To avoid ambiguity, here is some terminology specific to Corinna:
- Slot: A place where class or instance data is stored
- Slot variable: A variable which contains slot data
- Slot attribute: An attribute which extends the class behavior in relation to a given slot.
- Slot modifiers: alternative ways of declaring a slot that have different meanings
For example, from the Cache::LRU
code above:
has $created :reader = time;
The has
keword declared the slot variable $created
. This variable contains
the slot (or data) for this class. The :reader
attribute tells the class
that there will be an accessor named created
:
my $cache = Cache::LRU->new( max_size => 40 );
say $cache->created;
(You can change the name of the accessor. More in the "Attributes" section below).
To avoid ambiguities in the grammar, we have defined one:
Corinna ::= CLASS | ROLE
CLASS ::= DESCRIPTOR? 'class' NAMESPACE
DECLARATION BLOCK
DESCRIPTOR ::= 'abstract'
ROLE ::= 'role' NAMESPACE
DECLARATION ROLE_BLOCK
NAMESPACE ::= IDENTIFIER { '::' IDENTIFIER } VERSION?
DECLARATION ::= { PARENTS | ROLES } | { ROLES | PARENTS }
PARENTS ::= 'isa' NAMESPACE { ',' NAMESPACE }
ROLES ::= 'does' NAMESPACE { ',' NAMESPACE } ROLE_MODIFIERS?
# role grammar is not final
ROLE_MODIFIERS ::= '<' ROLE_MODIFIER {ROLE_MODIFIER} '>'
ROLE_MODIFIER ::= ALIAS | EXCLUDE | RENAME
ALIAS ::= 'alias' ':' METHOD
EXCLUDE ::= 'exclude' ':' METHOD
RENAME ::= 'rename' ':' METHOD '=>' METHODNAME
IDENTIFIER ::= [:alpha:] {[:alnum:]}
VERSION ::= 'v' DIGIT {DIGIT} '.' DIGIT {DIGIT} '.' DIGIT {DIGIT}
DIGIT ::= [0-9]
BLOCK ::= # Perl +/- Extras
# getting very sloppy here
ROLE_BLOCK ::= # 'requires' METHODNAMES BLOCK | BLOCK 'requires' METHODNAMES
# grammar constructs for methods
METHOD ::= ABSTRACT_METHOD | CONCRETE_METHOD
ABSTRACT_METHOD ::= 'abstract' 'method' SIGNATURE ';' # or empty block?
CONCRETE_METHOD ::= METHOD_MODIFIERS 'method' SIGNATURE '{' (perl code) '}'
SIGNATURE ::= METHODNAME '(' current sub argument structure + extra work from Dave Mitchell ')'
METHODNAMES ::= METHODNAME { METHODNAME }
METHODNAME ::= [a-zA-Z_]\w*
METHOD_MODIFIERS ::= METHOD_MODIFIER { METHOD_MODIFIER }
METHOD_MODIFIER ::= 'has' | 'private' | 'overrides' | 'multi' | 'common'
Note that because the BLOCK
contains Perl, we've, er, punted a bit on that
grammar section.
Because the class
block syntax does not exist in Perl's prior to whichever
Perl will first implement Corinna, it's backwards-compatible because it cannot
clash with previous versions of Perl (short of those doing very weird, crazy
things). So until you do this:
use feature 'class'; # use Corinna
You're safe.
While we're at it, for Corinna v1, the class BLOCK
, as described in the
grammar, assumes use strict
, use warnings
, and use utf8
.
Instance data for classes are in "slots." Slots are declare by: has $var;
.
Note We can allow has @var
and has %var
, but with no attributes.
They're not in this proposal with attributes because it's unclear what has @var :reader :writer;
means. Does it accept and return lists? It's also
required in the constructor in that version. Does it then require an array
reference? Due to the flattening nature of some variables in Perl, I'd rather
punt on this for the time being.
The has
keyword does not create any readers, writers, have anything to
with the constructors, and so on. It only declares the variable containing
the slot. It's the slot attributes which handle everything else.
has $x;
is a private slot. Absent other attributes modifying it, it is:
- Read-write (internally)
- Forbidden in the constructor
- Has no public reader or writer
has $x = $value;
supplies a default value. See also :builder
below.
Note that slots are lexically bound and cannot be seen in subclasses or in consumed role methods.
Attributes are for object construction and data modification, and helpers ("nice to haves" which make working with objects more pleasant).
Slots are defined via has $varname;
. This does nothing outside of
declaring the slot. Nothing at all. Instead, if we wish to specify arguments
for the constructor, we use the :new
syntax.
All slot variable values are assigned (if appropriate) in the order they are
declared. However, as soon as Corinna reaches a variable with a :new
attribute, assigning to variables halts until after the CONSTRUCT
phaser is
called. See CONSTRUCT
elsewhere in this document.
has $name :new;
If you wish the slot to be optionally passed in the constructor, you must provide a default value or a builder:
has $name :new = 'Ovid';
The default value, of course, may also be undef
if you do not need a value for that slot.
The "name" of a slot is the identifier name of the slot variable. Thus, the
name of has $person :new;
is person
. If you need it passed to the
constructor with a different name, use the :name(...)
attribute:
has $person :new :name(employee);
Absent a :new
attribute, the attribute must not be passed to the constructor.
Note that the :name
attribute also changes the default names of reader and
writer methods, though these can still be overridden on a case-by-case basis.
Attribute | Meaning | Notes |
---|---|---|
:reader , :reader($name)
|
Creates a read-only public method for the data | N/A |
:writer , :writer($name)
|
Creates a public method (set_$name ) for modifying the data |
This is frequently a code smell |
:predicate , :predicate($name)
|
Creates a has_$name boolean predicate |
What's the difference between uninitialized and undef ? |
:handles(@list|%kv_pairs) |
Delegates the methods to the object in this slot | Requires an object! |
:name($identifier) |
Public name of slot | You cannot use :name on a multi-slot declaration |
The writer creates a method called set_$name
to avoid overloading the
meaning of the method name, and will return the invocant. Setting
:reader($name)
and :writer($name)
to the same $name
would be an error.
This is in part because Corinna would need to special case that and have to write
more complicated internal code.
See Custom Writers for more explanation.
(Arguably, the :writer
attribute could go here)
Attribute | Meaning | Notes |
---|---|---|
:builder , :builder($name)
|
Creates a builder for the slot. | Conflicts: = defaults |
:weak |
Weakens the variable in the slot | N/A |
:clearer |
Clears the slots. Sets it to undef if no default value |
Makes no sense without :reader ? |
The above seems to simplify this work quite a bit. Assuming :writer
to be a
code smell, the following are "valid" combinations that are likely to be seen.
Note that all of these allow the `:name(identifier) attribute.
Declaration | Constructor | Attribute |
---|---|---|
has $x; | No | No |
has $x :reader; | No | Yes |
has $x :new; | Yes | No |
has $x; | No | No |
has $x :reader :clearer; | No | Yes |
has $x :reader :new; | Yes | Yes |
has $x :reader ; | No | Yes |
has $x :builder ; | No | No |
has $x = $default; | No | No |
has $x :reader :new :clearer; | Yes | Yes |
has $x :reader :clearer; | No | Yes |
has $x :reader :builder ; | No | Yes |
has $x :reader = $default; | No | Yes |
has $x :reader :builder :clearer; | No | Yes |
has $x :reader :clearer = $default; | No | Yes |
We're working hard to ensure that you can't create an illegal set of attributes for a slot, but we still have a few).
-
:builder
and= $default
-
:builder
or= $default
with:new
-
:clearer
without:reader
(do we really care about this?)
Each object/class has only one intrinsic slot identity for each combination of name-and-package (where package is the name of the class or role in which the slot is originally declared). In other words, each slot is very much like a regular package variable, except per-object, rather than per-package...and explicitly declared, rather than inferred by usage.
Each has
declarator reifies the underlying intrinsic slot, but also declares
a lexically scoped per-object alias for that slot (much like our
creates a
lexically scoped per-block alias for a single package-scoped variable of the
current package). In other words, we distinguish the per-object intrinsic
slot from the extrinsic per-lexical-scope slot alias.
That also means that if there are two has $slot_name
declarations
in separate lexical blocks within the same class or role, those two
declarations merely create two distinct lexical aliases to the same
reified intrinsic slot. If both declarations also specify attributes that add
initializers or accessors, then those attributes are cumulative in effect
(and a fatal compile-time error if inconsistent in any respect). We may
revisit this.
This implies that the composition of any role that specifies its own slot simply adds the intrinsic slot (with identity name-and-rolename, not name-and-classname) to the composing class, but does not export the lexical alias for the slot into the composing class.
Thus, the methods defined in the role can access the composed-in intrinsic slot through its lexical alias within the role's block, but methods defined in any composing class cannot access the composed-in intrinsic slot directly.
If the class also directly declares a slot of the same name as one provided by a role, then that slot is distinct from any slot composed in from any role (because the intrinsic identity of the directly declared slot is name-and-type-and-classname, not name-and-type-and-rolename).
Note that this also (correctly) implies that base-class slots are not directly
accessible in derived classes or roles, because their intrinsic identities are
name-and-classname, and because the associated lexical aliases created by
their defining has
are lexically scoped to the block of the base class.
Methods, unlike subroutines, must be called on classes or instances and have
an implicit $class
(for common
methods) or $self
(for non-common
methods) variable injected into their scope. Until and unless we include a
multi
modifier (v2 at the soonest), it will be illegal to ever declare for a
given class more than one method with the same name).
Instance methods are declared with the method
keyword.
method full_name ($optional_title='') {
return $optional_title ? "$optional_title $name" : $name;
}
Though not shown in the above, a $self
variable is automatically injected
into the body of the method.
Class methods use the common
keyword:
common method remaining () {
return $MAX - $count;
}
Note that a $class
variable is injected into the above method.
See "Class and Instance Methods/Slots" for more details.
It's important to note that methods are not subroutines. Thus, this is not a problem in Corinna:
class Accumulator {
use List::Util 'sum';
has $numbers :reader = [];
method add_number($number) {
push$numbers->@* => $number;
}
method sum () {
return sum($numbers->@*);
}
}
Further, if there were no sum()
method in the above class, but the sum()
function was imported, calling $accumulator->sum
would result in method not found
error.
Any method
keyword can be prefixed by one of several modifiers. These
modifiers may be combined into multiple modifiers such as:
has private override method foo ($arg) { ... }
common override method bar () { ... }
The has
and common
modifiers are mutually exclusive.
This is the default and it's optional. It's an instance method.
has method foo() { ... }
# same as
method foo() { ... }
This is a class method (you can call it at the class level, not just the instance level). Class methods can access class data, but not instance data.
common method foo() { ... }
This method must override a parent class method. If the parent class responds
false to $parent->can($method->name)
, this modifier will throw an exception.
This method is file-scoped. It may only be called from the methods actually defined in this package. Even methods in consumed roles can not call this methods. Subclasses may also not call these methods.
Anything outside of the class defining a private method which attempts to call that prviate method will generate a "method not found" error.
private method some_instance_method () { ... }
common private method some_class_method () { ... }
That being said, a subclass can't create a method with the same name as a parent private method due to this:
class Parent {
private method do_it () { ... }
method do_something () {
$self->do_it;
}
}
class Child isa Parent {
method do_it () { ... }
}
If you call my $o = Child->new
and then call $o->do_something;
, does
do_something
call the parent or child do_it
method? The child doesn't know about
the parent method, so it's allowed to have a semantically different do_it
.
For method resolution, do we make a special case for private methods?
Thus, until (if) we resolve this, we'll need a new type of exception for a method you're not allowed to create.
These are phases, not methods. They are analogous to the Moo/se BUILDARGS
,
BUILD
, and DEMOLISH
methods. However, the names are changed to make it
clear to developers that their semantics are different.
Pseudo-code of the phases of a class start when ->new
is called.
Create an instance
foreach class/instance variable in order declared {
if :new is found {
gather remaining :new variables into %args for CONSTRUCT
call CONSTRUCT(%args)
}
initialize if not initialized via CONSTRUCT
}
call ADJUST
return instance
Upon destruction:
Call DESTRUCT with a UNIVERSAL::Cor::DESTRUCTION instance
undefine all variables in reverse order declared
Of the above, we currently only intend to support hooking into the
CONSTRUCT
, ADJUST
, and `DESTRUCT, phases.
The following discussion will reference this Box
class:
class Box {
common $num_boxes :reader = 0;
has $created = time;
has ( $height, $width, $depth ) :new :reader;
has $after_construction = time;
has $volume :reader :builder;
# if you leave off 'private', this can be overridden in a subclass
private method _build_volume () {
return $height * $width * $depth;
}
# called before initialization. No instance variable required in the
# constructor has a value at this
CONSTRUCT (%args) {
if ( exists $args{side} ) {
my $length = delete $args{sides};
$args{height} = $length;
$args{width} = $length;
$args{depth} = $length;
}
return %args;
}
# called after initialization.
# yes, this example is silly
ADJUST (%args) { # same arguments as CONSTRUCT accepts, not returns
if (exists $ENV{MAX_VOLUME} && $volume > $ENV{MAX_VOLUME}) {
croak("$volume is too big! Too big! This ain't gonna work!");
}
$num_boxes++;
}
DESTRUCT($destruct_object) {
$num_boxes--;
}
}
The CONSTRUCT
phaser is used when you want to change the behavior of
constructor arguments. Every CONSTRUCT
method accepts an even-sized list of
arguments and is expected to return an even-sized list. The even-sized list
passed to CONSTRUCT
is whatever was passed to the new
constructor:
my $box = Box->new(...);
If you wish to provide a single value constructor, you must create a
constructor not named new
:
my $cube = Box->new_cube(7);
All CONSTRUCT
phasers are called from parents to children. If multipler
inheritance is allowed, it will be in reverse MRO order. There is no need to
explicitly handle this. Simply create your phaser and be done with it.
For example, imagine you have a Box
class:
my $box = Box->new( height => 7, width => 13, depth => 22.5 );
say $box->volume; # 2047.5
But you would like to special case a cube. You can pass a single argument and that will be used for the heigth, width, and depth:
my $cube = Box->new( side => 15 );
say $box->volume; # 3375
You can do this by mapping the side => 15
argument to the appropriate
values in the CONSTRUCT
phaser:
# called before initialization. No instance variable required in the
# constructor has a value at this
CONSTRUCT (%args) {
if ( exists $args{side} ) {
# you still probably want a check for something like
# if ( 1 == keys %args ) { ... }
my $length = delete $args{side};
$args{height} = $length;
$args{width} = $length;
$args{depth} = $length;
}
return %args;
}
Note that none of the instance variables required in the constructor will have a value at this time. However, class and instance variable declared before these variables will have values, while ones after it will not have values.
Let's look at a concrete example:
common $num_boxes :reader = 0;
has $created = time;
has $some_var :builder;
has ( $height, $width, $depth ) :new :reader;
has $after_construction = time;
has $volume :reader :builder;
common $answer = 42;
By the time we get to the line with the :new
attribute, $num_boxes
,
$created
.
Then CONSTRUCT
method will be called. If you do not provide one, a copy of
the default UNIVERSAL::Cor::CONSTRUCT
phaser will be flattened into your
class. If the return value of CONSTRUCT
does not contain keys for height
,
width
, and depth
, an error will be thrown. If those keys are present, they
will be used to assign to their respective slot variables. Any extra keys are
ignored if you have subclasses as those might be passed to child CONSTRUCT
phasers to initialize them.
The $after_construction
construction variable will not have a value when
CONSTRUCT
is called, nor will the $volume.
The $answer
variable will not have a value if CONSTRUCT
is called for the
first time because this is a class variable. Subsequent calls to CONSTRUCT
will have that variable defined, but it's relying on the value of variables
without :new
in the CONSTRUCT
is not recommended.
Regarding the last point, there is nothing stopping the :builder
on
$other_var
from setting those other variables, but this is strongly not
recommended.
As an aside, for altering our constructor to accept side => $value
in
Moo/se, this is how it might be done:
around 'BUILDARGS' => sub {
my ($orig, $class) = (shift, shift);
my $args = $class->$orig(@_);
if ( exists $args->{side} ) {
my $length = delete $args->{side};
$args->{height} = $length;
$args->{width} = $length;
$args->{depth} = $length;
}
return $args;
}; # <--- don't forget that semicolon again!
When the CONSTRUCT
phaser returns it values, they get assigned where
appropriate, and the rest of the variables are assigned.
The ADJUST
phaser is called after object construction, but immediately before the
instance is returned from new
. The default implementation does nothing. It
is called from parent to child, in reverse MRO order.
It receives the same arguments as CONSTRUCT
. Note that keys are unchanged,
but if any values were references and they were altered in CONSTRUCT
, they
will be altered in ADJUST
, too. We don't do deep cloning here.
In our Box
example:
ADJUST (%args) { # same arguments as CONSTRUCT accepts, not returns
if (exists $ENV{MAX_VOLUME} && $volume > $ENV{MAX_VOLUME}) {
croak("$volume is too big! Too big! This ain't gonna work!");
}
$num_boxes++;
}
Return values from ADJUST
are discarded.
All DESTRUCT
phasers are called from children to parents in MRO order.
This phaser is called during instance and global destruction. It allows you
to take additional, important action. In the Box
class, we merely reduce the
$num_boxes
class variable by one:
DESTRUCT($destruct_object) {
$num_boxes--;
}
The DESTRUCT
method accept a UNIVERSAL::Cor::DESTRUCTION
instance (class
anme TBD). This class looks like this (conceptually. We might not allow people
to instantiate it directly).
class UNIVERSAL::Cor::DESTRUCTION {
common $in_global_destruction :reader :new;
has $construct_args :reader :new;
}
Thus, you could do things like:
DESTRUCT ($destruction) {
if ( $destruction->in_global_destruction ) {
# clean up all resources used
# disconnect from db. Etc.
}
}
Roles are similar to Moo/se roles and are declared with the role
keyword.
However, we need to first review our tentive grammar. We used one which is a
bit unusual to make it clear this isn't standard behavior. This is the part of
the Corinna specification I am the least comfortable with.
ROLES ::= 'does' NAMESPACE { ',' NAMESPACE } ROLE_MODIFIERS?
# role grammar is not final
ROLE_MODIFIERS ::= '<' ROLE_MODIFIER {ROLE_MODIFIER} '>'
ROLE_MODIFIER ::= ALIAS | EXCLUDE | RENAME
ALIAS ::= 'alias' ':' METHOD
EXCLUDE ::= 'exclude' ':' METHOD
RENAME ::= 'rename' ':' METHOD '=>' METHODNAME
So a class could do something like this:
class MyClass does MyRole <exclude: foo,
exclude: bar,
alias: one => uno,
rename: this => that> {
# class body here
}
The exclude
simply removes that method from this role application.
The alias
gives an alias for a method name, but does not remove it from the
role.
The rename
is a combination of exclude
and alias
.
Here's a simple role:
roles Role::Serializable::JSON {
use Some::JSON::Module 'to_json';
requires 'to_hashref';
has $some_arbitrary_var; # unused here
method to_json () {
my $hashref = $self->to_hashref;
return to_json($hashref);
}
}
And a class can consume that with:
class Person isa Shiny::ORM does Role::Serializable::JSON {
has $mine;
method to_hashref() { ... } # because the role requires it
...
}
In the above, the Person
class cannot access the role's
$some_arbitrary_var
slot variable and the role cannot access the Person
class's $mine
variable.
Per the grammar (described elsewhere), roles can consume multiple roles and classes can consume multiple roles. Role consumption follows the rules described in Traits: The Formal Model.
The formal model states that trait composition must be commutative (section
3.4, proposition 1). This means that: (A + B) = (B + A)
.
The formal model also states that trait composition must be associative
(section 3.4, proposition 1). This means that (A + B) + C = A + (B + C)
.
In other words, no matter how you mix and match your roles, if a a given set of consumed roles is identical, their semantics must be identical.
Note that for the above, a role is defined by its namespace plus the set of methods it provides. For example, if we exclude a role method:
class SomeClass does SomeRole <exclude: some_method> {...}
Then the role consumed by SomeClass
is not the same as SomeRole
because
that role includes the some_method
method.
What this means is that Corinna avoids the thorny trap of Moose's Composition Edge Cases. For example, in Moose,
In short, Moose is associative if and only if you do not have multiple methods with the same name. In Moose, if a role providing method M consumes one other role which also provides method M, we have a conflict:
package Some::Role;
use Moose::Role;
sub bar { __PACKAGE__ }
package Some::Other::Role;
use Moose::Role;
with 'Some::Role';
sub bar { __PACKAGE__ }
package Some::Class;
use Moose;
with 'Some::Other::Role';
package main;
my $o = Some::Class->new;
print $o->bar;
However, if the role consumes two or more other roles which provide the same method, we don't have a conflict:
package Some::Role;
use Moose::Role;
sub bar { __PACKAGE__ }
package Some::Other::Role;
use Moose::Role;
sub bar { __PACKAGE__ }
package Another::Role;
use Moose::Role;
with qw(Some::Role Some::Other::Role);
sub bar { __PACKAGE__ }
package Some::Class;
use Moose;
with 'Another::Role';
package main;
my $o = Some::Class->new;
print $o->bar;
This is because, in Moose, when you have two or more roles consumed, any conflicting methods are excluded and considered to be requirements.
Corinna skips this confusion by going back to the original trait behavior as defined by the trait researchers (and confirmed by Ovid in email to them): a class providing method M which consumes a role or set of roles providing a method M must explicitly resolve the conflict:
class Some::Class does Some::Role <exlude: M> {
method M () {...}
}
Failure to do so is a compile-time failure.
At the present time, roles should support DESTRUCT
phasers. It should be
guaranteed at the time that the role is called that any slots it relies on
should have been defined. This is less clear for ADJUST
and CONSTRUCT
.
All Corinna classes have UNIVERSAL::Cor
as their ultimate base class.
Default phasers for CONSTRUCT
, ADJUST
, and DESTRUCT
are flattened into
any Corinna class which does not provide an implementation for them. This
implies that UNIVERSAL::Cor
should be a role, but it is not. The phasers
have special behaviors to avoid some of the ugly workarounds found in Moose.
We do not want UNIVERSAL::Cor
to be a role because sequencing of phasers and
methods is extremely important.
This is a first-pass suggestion of the Corinna object behavior. It provides some
basic behavior, but hopefully with sensible defaults that are easy to
override. In particular, it would 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_name) { ... } # Returns a subref
method does ($role_name) { ... } # Returns true if invocant consumes listed role
method isa ($class_name) { ... } # Returns true if invocant inherits listed class
# these new methods are not likely in v1, but the method names should be
# considered reserved. You can override them, but at your peril
# suggested. These can be overridden
method to_string () { ... } # overloaded?
method clone (%kv) { ... } # shallow
method object_id() { ... } # unique UUID
common method meta () { .. }
# these are "phases" and not really methods. They're like `BEGIN`, `CHECK`
# and friends, but for classes
CONSTRUCT { ... } # similar to Moose's BUILDARGS
ADJUST { ... } # similar to Moose's BUILD
DESTRUCT { ... } # similar to Moose's DEMOLISH
}
Currently, we use the common
keyword to identify class slots and class
methods. For (a silly) example, imagine a class that only allows 10 instances
of it:
class Foo {
my $max = 10;
# counter is the number of instances of this class
common has $counter :reader = 0;
CONSTRUCT (%args) {
if ( $counter >= $max ) {
croak("You cannot have more than $max instances of this class");
}
}
ADJUST (%args) { $counter++ }
DESTRUCT ($destruction) { $counter++ }
common method remaining() { return $max - $counter }
}
my $foo1 = Foo->new;
my $foo2 = Foo->new;
say Foo->remaining; # 8
say $foo1->remaining; # 8
undef $foo1;
say Foo->remaining; # 9
say $foo2->remaining; # 9
Note that you can call class methods on class names, subclass names, or instances. However, if you attempt to call an instance method using a class name or subclass name, you will get runtime error from Corinna (in other words, the developer does not need to remember to write their own error message for this).
The common
keyword has been provisionally chosen because the alternatives
seemed worse. We're open to suggestions.
Here are some alternatives and why they were rejected:
-
class
: this would overload the meaning of this keyword -
shared
: rejected because it seems to imply threads -
static
: used in other languages, but it's not immediately clear that it means "this is shared across classes"
At the present time, multiple inheritance with C3 MRO is assumed. Single inheritance is preferred.
Also, Corinna classses cannot inherit from non-Corinna classes due to
difficulties with establishing the base class (UNIVERSAL
or
UNIVERSAL::Cor
?). However, delegation is generally a trivial workaround.
Types were part of the original scope of work. They've been omitted because the work with types spans the entire language and any attempt by Corinna to address these issue
It's very hard to say because Paul Evan's Object::Pad has been the main testbed for these ideas. However, my local benchmarks have shown that while object construction appears to be a touch slower than core Perl, object runtime was faster. I assume this is due to having a pad lookup rather than hash dereferencing, but Paul can comment on that.
Watch this space ...
There are many things we can consider for v2.
-
:lazy
attributes for slots -
ADJUST
andCONSTRUCT
for roles - Authority declarations
- MOP
- Trusts
Note: the following list is generally of people who have commented enough about Corinna to have influenced my thinking about it, if not the actual design. They're presented in alphabetical order. My humblest apologies for those I've left out.
- Damian Conway
- Dan Book
- Darren Duncan
- Graham Knop
- Matt S Trout
- Paul Evans
- Sawyer X
- Stevan Little
- Toby Inkster
Corinna—Bringing Modern OO to Perl