-
Notifications
You must be signed in to change notification settings - Fork 19
Corinna Overview
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."
Corinna MVP (Minimum Viable Product)
This is intended to be the "canonical" description of Corinna behavior for Corinna version v.0.1.0. It's now intended to be a guide towards an MVP, not a full-and-complete guide to Corinna. If other documents on the wiki disagree, this document should be considered the correct one.
This is Corinna MVP Version 8
This is a version number for this document. Not for Corinna. "Changes" are listed near the end of this document.
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;
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 {
if ( $max_size < 1 ) {
croak(...);
}
$num_caches++;
}
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
}
}
TIMTOWTDI is great until it's not. For the MVP of Corinna, we're going to shoot for TIOWTDI (there is one way to do it). This will help us avoid ambiguity and, in many cases, offer a "slimmed down" OOP system. If we offer too much up front, people might start relying on that in their code. If it turns out to be a mistake, we might be stuck (witness the behavior of SUPER.
None of these need to be permanent, but changes to Corinna should go through the P5P RFC process.
Here are some restrictions in Corinna:
Single inheritance is much easier to implement and makes MRO heuristic issues go away. OOP code reuse is via roles and delegation.
We will use semantic versions, but only using the "version core" part of the grammar, preceded by a lower-case v:
<valid semver> ::= "V" <version core>
<version core> ::= <major> "." <minor> "." <patch>
<major> ::= <numeric identifier>
<minor> ::= <numeric identifier>
<patch> ::= <numeric identifier>
<numeric identifier> ::= "0" <positive digit> | <positive digit> <digits>
<digits> ::= <digit> | <digit> <digits>
<digit> ::= "0" | <positive digit>
<positive digit> ::= "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9"
Imagine this:
class Order does Role::VAT, Role::PremiumDiscount {
...
}
The VAT adds 20% to the order value while the Premium Discount might take €10 off the price of an order in some situations. The total cost of a €100 order would be €108 or €110, depending on the order in which VAT and discount are applied. If the VAT and discount are applied via method modifiers, then the order in which you consume the roles can break your code! This is one of the many problematic behaviors form multiple inheritance and mixins that we're trying to avoid.
Yes, we'll likely have method modifiers in a subsequent RFC, but not for the MVP.
We're likely to have special behavior in a new object base class, but if we inherit from, say, DateTime
, which base class do we get?
To work around this limitation, we can do this:
has $created :handles(*) = DateTime->now;
The special handles(*)
syntax says "if I don't have a method for this, try to call it on the $created
object. If more than one slot has handles(*)
(thus, simulating MI), we try the slots in the order defined.
my $thing = Object->new( name => 'foo' );
my $thing = Object->new( { name => 'foo' } ); # illegal
my $thing = Object->new('foo'); # illegal
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
-
OBJECT
type: Callingref $corinna_object
should returnOBJECT
, a new reftype.
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 (note that method modifiers may be v2)
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' | 'abstract'
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.
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).
Full pseudo-code with precise details of object construction is here.
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.
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 have been an
error. This is in part because Corinna would need to special case that and
have to write more complicated internal code. As mentioned in the actual
RFC, the wiki is here for historical interest
and should not be relied upon as the canonical description.
However, we now plan to reluctantly special-case this. Again, everything in the Wiki is a draft.
See Custom Writers for more explanation.
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 :new; | Yes | Yes |
has $x :reader ; | No | Yes |
has $x = $default; | No | No |
has $x :reader = $default; | No | Yes |
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 both $class
and $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).
Calling a method on an OBJECT
type will tentatively require that the method
in question be an actual method
and not a sub
.
If you define my $self
or my $class
inside of a method which has those
methods injected, you should get a "redefined" warning.
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.
These are phases, not methods. They are analogous to the Moo/se BUILD
, and DEMOLISH
methods. However, the names are changed to make it
clear to developers that their semantics are different.
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 = $height * $width * $depth;
common method new_cube ($side) {
return $class->new( height => $side, width => $side, depth => $side );
}
# called after initialization.
# yes, this example is silly
ADJUST {
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 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.
This phaser has access to all instance variables, including $self
and $class
.
In our Box
example:
ADJUST {
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
might need to become a method, not a phaser, due to passing in an object).
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::Corinna::DESTRUCTION
instance (class
name TBD). This class looks like this (conceptually. We might not allow people
to instantiate it directly).
class UNIVERSAL::Corinna::DESTRUCTION {
has $construct_args :reader :new;
method in_global_destruction () {
# requires 5.14
return 'DESTRUCT' eq ${^GLOBAL_PHASE};
}
}
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 }
So a class could do something like this:
class MyClass does MyRole, MyOtherRole {
# class body here
}
Here's a simple role:
roles Role::Serializable::JSON {
use Some::JSON::Module 'to_json';
method to_hashref (); # requires
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.
For future versions of Corinna, a role should be 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.
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
.
Note: the following is a WIP.
All Corinna classes have UNIVERSAL::Corinna
as their ultimate base class.
Default phasers for ADJUST
, and DESTRUCT
are flattened into
any Corinna class which does not provide an implementation for them. This
implies that UNIVERSAL::Corinna
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::Corinna
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.1.0 {
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 meta () { .. }
# these are "phases" and not really methods. They're like `BEGIN`, `CHECK`
# and friends, but for classes
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;
ADJUST {
if ( $counter >= $max ) {
croak("You cannot have more than $max instances of this class");
}
$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, single inheritance is assumed.
Also, Corinna classses cannot inherit from non-Corinna classes due to
difficulties with establishing the base class (UNIVERSAL
or
UNIVERSAL::Corinna
?). However, delegation is generally a trivial workaround. We
intend special-case code for this:
has $created :handles(*) = DateTime->now;
It's very hard to say because Paul Evan's Object::Pad has been the main test bed 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
for roles -
Authority declarations
-
MOP
-
Trusts
-
All method modifiers (
private
,common
, etc. See below.) -
Runtime role application
At the present time, method modifiers are likely v2
Any method
keyword can be prefixed by one of several modifiers. These
modifiers may be combined into multiple modifiers such as:
has private overrides method foo ($arg) { ... }
common overrides 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 be overridden in a subclass. It's a compile-time failure if it is not. Can only be declared in an abstract class.
abstract method fumigate($args);
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 block-scoped for the current class. This means it may only be called from the methods actually defined in this class and file. 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 private 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.
For a more detailed background, read this article.
In short, slots are not encapsulated if children can override the parent value and the parent isn't able to ensure that the new value is correct.
People have asked "how can I override a parent attribute in my subclass without an overrideable builder?"
There are multiple approaches to this. Here's one.
First, imagine a simple Moose Collection
class that allows you to call
_build__index
. The example is silly, but it makes it easy to demonstrate
the issue. Further, people might argue that you shouldn't do this, but in
reality people do this sort of thing all the time, so let's at least make it
safer.
In our example, if a child overrides _build__index
to return a value
greater than the number of elements in items
, this class is broken (if
it's not immediately obvious why, then that should be a clue as to why
overridding parent slots is a bad idea).
package Collection {
use Moose;
# this is a personal module which gives me a "saner" Perl
# environment
use Less::Boilerplate;
use Types::Standard qw(Int ArrayRef);
has _index => (
is => 'rw',
isa => Int->where('$_ >= 0'),
builder => '_build__index',
);
# default, but maybe someone wants a different default
sub _build__index { 0 }
has items => (
is => 'ro',
isa => ArrayRef,
required => 1,
);
sub BUILD ( $self, @ ) {
my @items = $self->items->@*;
my $type = ref $items[0];
foreach my $item (@items) {
if ( not defined $item ) {
croak("items() does not allow undefined values");
}
if ( ref $item ne $type ) {
croak("All items in collection must be of the same type");
}
}
}
sub num_items ($self) {
return scalar $self->items->@*;
}
sub next ($self) {
my $i = $self->_index;
return if $i >= $self->num_items;
$self->_index( $i + 1 );
return $self->items->[$i];
}
sub reset ($self) {
$self->_index(0);
}
}
For v0.1.0
of Corinna, it's still easy to override that, but we do so in
ADJUST
:
class CollectionWithoutBuilder {
has $index;
has $items :new;
ADJUST (%args) {
$index = $self->_default_index;
}
method _default_index () { 0 }
# other methods here
}
As you can see, we provide the same behavior as :builder
, but now we have
fine-grained control over when it's called (something that's hard to do in
Moo/se).
Contrast that to what we had before:
class CollectionWithBuilder {
has $index :builder;
has $items :new;
method _build_index () { 0 }
# other methods here
}
The above is arguably wrong because $index
would be initialized before
$items
, but it depends on $items
being defined so that we can check its
upper-bounds. We don't do it here because we've hard-coded the value of zero,
but our children can easily replace that.
However, the CollectionWithoutBuilder
class is still broken because we
allowed the child to override the $index
, but we don't validate it. So let's
do that:
class Collection {
has $index;
has $items :new;
ADJUST {
$index = $self->_default_index;
if ( $index < 0 || $index > $items->@* - 1 ) {
croak(...);
}
}
method _default_index () { 0 }
# other methods here
}
Now, children can safely override that value because our code breaks if we supply an incorrect value (ignoring the lack of types). And in this particular case, the order in which our slot variables are declared is no longer relevant. If our checks become more complicated, we have one canonical place to insert them.
(Note: we'd also want checks on $items
for the above, but those were omitted
to focus on the main issue)
This is not to say that :builder
or an analogue won't be in future versions,
but we do not plan to support it for the MVP.
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.
- Chris Prather
- Damian Conway
- Dan Book
- Darren Duncan
- Graham Knop
- John Napiorkowski
- Matt S Trout
- Paul Evans
- Sawyer X
- Stevan Little
- Toby Inkster
- 2021/06/13
- Remove
CONSTRUCT
phaser. - Show forward declarations for role requirements.
- Listed restrictions in the MVP
- Renamed
UNIVERSAL::Cor
toUNIVERSAL::Corinna
. - Speculated that we might not need a base class
- 2021/03/01
-
:builder
has been removed from the MVP. See this article.
- 2021/02/27
- Instance methods have both
$self
and$class
injected. - Redeclaring
$self
and/or$class
in methods which already have that should generated a "redefined" warning
- 2021/02/26
- Add version number and Changes section
- Remove :clearer. Internally we just just set the variable to what's needed
- Change the focus of this document to v0.1.0, not v1.0.0
- 2021/02/24
- Move method modifiers to v2
- 2021/02/22
- Move multiple features to "v2"
-
:lazy
attributes for slots -
ADJUST
for roles - Authority declarations
- MOP
- Trusts
-
- 2021/02/21
- Clarify DESTRUCT and ADJUST call order
- Create better BUILDARGS example
- Allow
has @var
andhas %var
if there are no attributes - Updated UNIVERSAL::Corinna description
- 2021/02/21
- Corinna Overview document
- Officially named "Cor" to "Corinna"
Corinna—Bringing Modern OO to Perl