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
}
}
So I see now better why you have the defaults the way you do, I'm still not sure I agree but I'll get back to that. I'm not sure how I feel about the transient nature of sigils in this proposal.
Moose culturally and syntactically tried to avoid exposing attribute slots (and all that comes with them) because they were trying to break people of the habit of treating the instance as "just a hash". One of the things I (eventually) liked about Stevan's recent proposals was that slot creation was "just creating a lexical". It made a lot of intuitive sense once you got your head around it "not being Moose", and injecting behaviors into the object were layered on top of that.
It seems to me that
has $foo
andhas 'foo'
both be valid but different is gonna be hard for different groups of people to wrap their heads around. In the case of people with some experience in legacy codehas $foo
being the new thing but suddenly doesn't generate an accessor will be (I'd bet hard credits) surprising. For example ishas $foo :rw
an exception? If so why, if not why not? Conversely people totally new to Perl suddenly seeinghas 'foo'
as a thing whilemy 'foo'
gives an error is also going to be (take more of my hard credits) surprising. Especially since there is all this old code that hashas foo => ( ...)
all over it. Trying to keep consistency with Moose, in my opinion here, loses consistency with Perl[1].This leads to my earlier issue which I explained badly (I was on a phone in the middle of a grocery store), the common case for most read-only attributes is to have data that is read after being initialized at object creation time. The "struct" style object:
This makes the (IME) common case require the most typing. It also means that
Device->new(serial_number => $sn, location => $location)
will blow up because it's nether:required
nor:optional
[2] but with no warning to that effect in the class definition. I was suggesting that you make either:required
or:optional
the default and make no constructor args require an attribute.Optional doesn't have to be the default[3], but given the way objects have thus far worked in Perl optional seems more ... useful. This would optimize (IMO) for the common case for how people work with objects (not just in Perl but in most of the languages I've used).
I like where you're going with the code-attributes on slot creation. I think that simplifying the entire system closer to where Stevan was going in Moxie makes more sense (at least to me):
Then attribute slots look like and are treated exactly like variables. With this system I'd probably re-arrange the defaults some:
IMO this is easier to explain to both new people and experienced people alike. There's no weird conditional values based on sigil presence, struct-style objects are still the default, and you have to explicitly state how you want to deviate from that default in the class definition so there's some nominal "self-documentation".
1: In that same vein does
has [qw/height width depth/] :required;
mean with sigils it'shas [qw/$h $w $d/] :required
? Wouldn't that be better just making it work like every other kind of variable declaration?has $h, $w, $d :required;
andhas qw(height width depth) :required
? The only reason Moose made things an array ref there was it was because has() is just a function. I'm like 99% positive if they'd had access to the perl core like this Stevan and Yuval, would have made it work more like variable declaration instead.2: In fact does
location
have any way to set the value here at all? it has no sigil somethod update_location { location = shift() }
doesn't look right … andmethod update_location { self->location = shift() } will make people think
$obj->location = $location` should work too.3: I personally would prefer "required" to be the default, it keeps with the WORM/Immutable object principle we should be encouraging people to adopt.