This is a rough draft of some thoughts I've had regarding Cor attributes declaration. Please leave your thoughts.
Part of the problem with the Cor object proposal for the Perl
code is that
we tended to use the semantics of has
as declared in the
Moose OO extension for Perl. Unfortunately,
this function handles:
- Data
- Attributes
- Types
- Coercion
- Delegation
- Clearers
- Predicates
- Documentation
- Constructor args
- Default values
- Overriding
- … and more!
It makes it very, very easy to declare attributes for Perl objects, but it's trying to do too much and has the wrong defaults. Instead, I've been rethinking this tremedously, trying to find a way to keep the "ease of use", but making it easier to do the right thing.
There have been suggestions that we separate slot (data) declaration from the slot's attribute declaration, but I think this is a mistake. We have literally hundreds of modules on the CPAN which try to join the two together. If we break them back apart, people will try to put them back together and Cor will again cause fragmentation of approaches on how to build objects.
My idea is to turn has
into a variable declarator similar to my
. It would
exist in the context of a Cor class block. Here's a minimal 2D point object
with x/y attributes, defaulting to 0 each, and with directly immutable
attributes (yes, it's a silly example):
class Point2D {
has [qw/x y/] :optional = (0,0);
# objects can mutate their own state
method move ($dX, $dY) {
$self->x($self->x + $dX);
$self->y($self->y + $dY);
}
}
The syntax is loosely:
has ::= 'hash' TYPE SLOTS ATTRIBUTES DEFAULT ';'
TYPE ::= # probably punting on this for an MVP
SLOTS ::= '[' SLOT {SLOT} ']' | SLOT
SLOT ::= SIGIL? IDENTIFIER
SIGIL ::= '$' | '@' | '%' | '*'
IDENTIFIER ::= [:alpha:] {[:alnum:]}
ATTRIBUTES ::= { ':' IDENTIFIER }
DEFAULT ::= '=' PERL_EXPRESSION
Slot behaviors:
- Read-only by default
- Defaults are lazy unless
:immediate
is provided - Have no accessor if a
SIGIL
is used
Current attributes:
:required
: slot must have a value at object construction:optional
: slot may have a value at object construction- Neither
:required
or:optional
: slot must not have a value at object construction :immediate
: default value is required and will be calculated at object construction:weak
: value is a weak ref.:builder
,:builder(name)
: A builder method (default:_build_$slot_name
) will provide the default:clearer
,:clearer(name)
: Reset slot toundef
or default value.:predicate
,:predicate(name)
: Test if slot has been set (but it may have been set toundef
):rw
: attribute is read-write (all classes are allowed to write their own data)handles(@|%)
: delegation
Examples:
class Box {
# all attributes are required to be passed to constructor
has [qw/height width depth/] :required;
# You can optionally name your box.
has 'name' :optional :predicate(has_name);
# cannot be set via constructor. Uses a lazy `_build_volume` method
has 'volume' :builder;
method _build_volumne {
return $self->height * $self->width * $self->depth;
}
}
Another example:
class Cache::LRU {
use Hash::Ordered;
# types probably won't be in v1
# sigil means that this attribute has no accessor. Hash::Ordered object is
# default
has Hash::Ordered $cache :handles(get) = Hash::Ordered->new;
# you may optionally pass in a max_size value to the constructor
# you can also call $cache->max_size to read this value or
# $cache->max_size($new_size) to mutate this value.
has PositiveInt :optional :rw max_size = 20;
# immediately record creation time
has created :immediate = time;
method set ( $key, $value ) {
if ( $cache->exists($key) ) {
$cache->delete($key);
}
elsif ( $cache->keys > $self->max_size ) {
# need the while loop in case they reset max size to a lower value
$cache->shift while $cache->keys > $self->max_size;
}
$cache->set( $key, $value ); # new values in front
}
}
I like choice 3. Currently the way we do this is to actually use a lexical.
This eliminates the "I have to create a (semi-public) accessor just to access slot data" problem that is a serious complaint about Moose. The only downside I can see to it right now is we're taking a currently fairly obscure idiom, and making it much more common.
Choice 2 seems to be a solution in search of a problem. The context surrounding them in Raku simply doesn't (yet) exist in Perl. Adding them without that context puts an extra burden on explaining what's going on. For example
has $!foo
works doeshas $.foo;
also work? What about$?FILE;
? What doesmy $!foo;
do? The answers to each of these is a maintenance burden no only on the scripts that usehas $!foo
but also on core to handle all the exceptions correctly.Choice 1 has an issue that I've run into a bunch in Moose. It presumes that the only kind of data you can store in a slot is scalar data. This leads to de-referencing everything. It gets ugly and tiresome, and with some less experienced programmers who don't understand why you get subtle bugs. That said if Choice 3 proves to be "too simplistic" then this might be an easy enough solution to fall back to (borrowing a bit from your syntax above):