Skip to content

Corinna Overview

Ovid edited this page Nov 7, 2021 · 52 revisions

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."

Title

Corinna MVP (Minimum Viable Product)

Disclaimer

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.

VERSION

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.

Name

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.

Example

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
    }
}

MVP Restrictions

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

Single inheritance is much easier to implement and makes MRO heuristic issues go away. OOP code reuse is via roles and delegation.

Versions are only v0.0.0

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"

No Method Modifiers

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.

No Inheritance from non-Corinna classes

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.

Constructors take an even-sized list of key/value pairs.

my $thing = Object->new( name => 'foo' );
my $thing = Object->new( { name => 'foo' } );    # illegal
my $thing = Object->new('foo');                  # illegal

Terminology

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: Calling ref $corinna_object should return OBJECT, 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).

Grammar

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.

Backwards Compatibility

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.

Slots

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.

Slot Attributes

Attributes are for object construction and data modification, and helpers ("nice to haves" which make working with objects more pleasant).

Object Construction

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.

Helper Attributes

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.

Valid Combinations

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

Detailed Slot Semantics

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

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.

ADJUST/DESTRUCT Phasers

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--;
    }
}

ADJUST

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.

DESTRUCT

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

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.

DESTRUCT

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.

UNIVERSAL::Corinna

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
}

Class and Instance Methods/Slots

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"

Inheritance

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;

Performance

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.

Future Work

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

Method Modifiers

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.

has

This is the default and it's optional. It's an instance method.

has method foo() { ... }
# same as
method foo() { ... }

common

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() { ... }

abstract

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);

override

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.

private

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.

No :builder for the MVP

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.

Contributors

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

Changes

  1. 2021/06/13
  • Remove CONSTRUCT phaser.
  • Show forward declarations for role requirements.
  • Listed restrictions in the MVP
  • Renamed UNIVERSAL::Cor to UNIVERSAL::Corinna.
  • Speculated that we might not need a base class
  1. 2021/03/01
  1. 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

  1. 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

  1. 2021/02/24
  • Move method modifiers to v2

  1. 2021/02/22
  • Move multiple features to "v2"
    • :lazy attributes for slots
    • ADJUST for roles
    • Authority declarations
    • MOP
    • Trusts

  1. 2021/02/21
  • Clarify DESTRUCT and ADJUST call order
  • Create better BUILDARGS example
  • Allow has @var and has %var if there are no attributes
  • Updated UNIVERSAL::Corinna description

  1. 2021/02/21
  • Corinna Overview document
  • Officially named "Cor" to "Corinna"