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
}
}
One thing I don't like about this proposal. We have two constructor attributes,
:optional
and:no-constructor
(maybe renamed to:private
) to define whether or not we should pass in values for certain slots (instance data). Lack of either of these means the slot data must be passed to the constructor. But having both:optional
and:no-constructor
should be an error (duh). But this is frustrating because I'm trying as hard as possible to ensure that we can't define object data in an invalid way. Having a single attribute for object construction would help. For example:But the above just looks sloppy. Having a single attribute with a parameter means I can't have two conflicting attributes. But I can't figure out the right attribute/parameter combinations that look "clean". Here's another awful suggestion:
But even that's not quite working because I can do this:
In the above example, we have a single attribute to define whether or not something must be passed to the constructor, but we have a useless default being declared.
This is a problem that languages like Java don't suffer from because their signature-based method overloading means that you can define constructors that do the right thing:
In the above example, we have two constructors. One takes three doubles and one takes one double. I don't have to mess around with declaring instance data as
:optional
. In fact, in the second constructor, I'm passing in data that doesn't even map to a slot.Since we're not going to get method overloading in Perl, we have to resort to some nasty hacks to allow different constructors (or manually create a constructor with a name other than
new
.Thoughts?