Skip to content

Slot Semantics

Ovid edited this page Sep 14, 2021 · 7 revisions

Please see the main page of the repo for the actual RFC. As it states there:

Anything in the Wiki should be considered "rough drafts."

Click here to provide feedback.

Slot Semantics

After a bit of discussion on #cor, it turns out that having slot declarations being lexical and using the slot identifier name as the public name is causing some issues.

Slots are private (which works in isolation), but NEW constructor needs to be able to "see" the slots in all of the lexical scopes if they have :new (unless NEW is implicitly in every class).

But what do we do for this?

class Parent {
    has $x :isa(Int) :new;
}

class Child isa Parent {
    has $x :isa(Str) :new;
}

In the above, we have a few issues.

First, if I call my $o = Child->new( x => 'bob'), what does the Child $x get? Internally, we have to build classes from the bottom to the top of the heirachy and the parent would get the child's type of the slot.

We also have similar issues with roles. There other issues, but that's a clear one that exposes all of the issues.

It seems like the problem here is that the variable holding the slot and the external name of the slot are tightly coupled.

The proposal to fix this is to decouple the name of the variable containing the slot and the attribute for the slot (similar to init_arg in Moo/se, but not quite the same).

I think this can be done with a new attribute called :name($identifier).

class Parent {
    # without a :name(...) attribute, the name is 'x'
    has $x :isa(Int) :new;
}

class Child isa Parent {
    has $x :isa(Str) :name(some_x) :new;
}

Now in the above, to construct it:

my $object = Child->new(
    x      => 42,
    some_x => 'DNA',
);

This means that for roles, we'd do the same thing. The slot variable would be lexically scoped, but the slot name would determine the name by which we read/write/construct the slot data.

We would need, for children overriding parent slot methods, to determine the exact behavior and syntax. A simple :has(+name) might work, with similar semantics to Moose?

A child could override the parent slot names, but role slot names would require the standard role exclusion and aliasing behaviors.

Lazy and Immediate

By default, builders are lazy. This means they will not be executed until such time that the value for that slot is needed:

has $cache :builder;

method _build_cache () {
    return Hash::Ordered->new;
}

For the above, what if we needed the cache instantiated at the same time the instance is instantiated? You can either supply an :immediate attribute, or (because the code is simple enough), you can inline the construction with =:

has $cache = Hash::Ordered->new;

Slot Ordering

Let's consider a simple Box class:

class Box {
    has ( $height, $width, $depth ) :new :reader :isa(PositiveNum);
    has $volume :reader :builder;

    # if you leave off 'private', this can be overridden in a subclass
    private method _build_volume () {
        return $height * $width * $depth;
    }
}

In the above, _build_volume is guaranteed to have defined values for $height, $width, and $depth. However, in Cor, we assume that slot values will be calculated in the order listed in the code: So we could make the above code even simpler:

class Box {
    has ( $height, $width, $depth ) :new :reader :isa(PositiveNum);
    has $volume :reader = $height * $width * $depth;
}

Because the $height and similar variables are guaranteed to be calculated first, $volume is guaranteed to have access to those values.

Any object destruction will destroy all values in the reverse order. Thus, if you need your database handle destroyed near the end of the object destruction cycle, put it near the top of your list of slot declarations.

(Note: I'm somewhat uncomfortable using slot definition ordering as meaningful, but making it deterministic in this manner is at least clearly understood).