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
    }
}
@jhthorsen
Copy link

I would remove “:optional”, and add “:required” instead since I mostly build default values for slots. Also, if a slot has a builder, then I think that should imply “:optional”. Or even better: Just make the slots without a builder or default value required.

I prefer :private, instead of :no-constructor.

@Ovid
Copy link
Author

Ovid commented Feb 29, 2020

Builder does imply optional. That's in my notes, but not reflected here. Oops!

And we have three states for constructor args: required, optional, and forbidden. Making "required" the default state helps with many "smart struct" type objects which are all about data. Even for those objects which are about "being experts", quite often you pass in several required args, so it seemed a sensible default. Not saying I'm stuck on this choice, however.

@Ovid
Copy link
Author

Ovid commented Feb 29, 2020

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:

has $x :new;             # required
has $x :new(optional);   # optional
has $x :new(no);         # not allowed in constructor

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:

has $x :constructor(yes);
has $x :constructor(no);
has $x :constructor(maybe);

But even that's not quite working because I can do this:

# required in the constructor but still has a default?
has $x :constructor(yes) = 3;

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:

class Box {
    double width, height, depth;

    Box(double w, double h, double d) {
        width = w; height = h; depth = d;
    }

    Box(double len) {
        width = height = depth = len;
    }

    double volume() {
        return width * height * depth;
    }
}

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?

@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