Skip to content

Instantly share code, notes, and snippets.

@Ovid
Last active March 1, 2020 12:44
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Ovid/c42fd0aee71ff57013afc6f3417d1324 to your computer and use it in GitHub Desktop.
Save Ovid/c42fd0aee71ff57013afc6f3417d1324 to your computer and use it in GitHub Desktop.
Cor attribute/slot declaration?

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 to undef or default value.
  • :predicate, :predicate(name): Test if slot has been set (but it may have been set to undef)
  • :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
    }
}
@Ovid
Copy link
Author

Ovid commented Mar 1, 2020

Or even better: Just make the slots without a builder or default value required.

@jhthorsen Not sure if we can safely do that or else we have the problem where we might want a truly private value which doesn't have a builder or default value, but which is computed at some odd point in the code using data that's not available until that moment. And default values and builders don't accept arguments, so we can't pass that data along.

@jhthorsen
Copy link

That’s why I liked “:private”.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment