Skip to content

Class Phases

Ovid edited this page Sep 14, 2021 · 6 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

Phases

Just as Perl code has BEGIN, CHECK, INIT, and other "pseudo subs" that fire at different times, so too does Corinna have class "phases". These special "pseudo-methods" are called at particular times in the instance lifecyle.

Every phase except CONSTRUCT has access to the $self variable. The phases are called in the following order:

  1. CONSTRUCT is called before attribute initialization. It receives the arguments passed to the constructor.
  2. NEW takes the arguments from every construct method and assigns the data to the instance variables.
  3. ADJUST is called after attribute initialization but before the instance is returned to the calling code. It receives the same arguments as CONSTRUCT
  4. DESTRUCT is called at object destruction.

Single Inheritance

Note that the following assumes that Corinna is single inheritance. We may allow multiple inheritance for a later release of Cor, but for the first pass, roles provide a suitable replacement for MI.

CONSTRUCT

All CONSTRUCT phases are called from root parent to child. Each receives the arguments passed to the constructor (usually expected to be an even-sized list) and is expected to return an even-sized list of key/value pairs to be used for the actual construction.

Returning an odd-sized list is a fatal error.

Example:

class SomeClass {
    has ($first, $second, $third ) :new;
    CONSTRUCT (@args) {
        if ( @args == 1 ) {
            my ( $first, $second, $third ) = split /:/ => $args[0];
            @args = (
                first  => $first,
                second => $second,
                third  => $third,
            );
        }
        return @args;
    }
}

NEW

New takes all of the lists of k/v pairs returned by CONSTRUCT phases and flattens them into a hash, with child pairs overriding parent pairs. Thus, if a child CONSTRUCT returns x => 2 and the parent CONSTRUCT return x => 'bob', the final k/v hash will contain x => 2, not x => 'bob'.

For V1, we might want to disallow developers creating their own NEW phase. Internally, it might look like the following pseudo-code:

NEW() {
    my %arg_for = $self->CONSTRUCT_ARGS;

    foreach my $class (from-parent-to-child) {
        foreach my $identifier (keys %arg_for) {
            $slot = get_slot($class, $identifier);
            $slot = $arg_for{$identifier};
        }
    }
}

ADJUST

ADJUST is also called from parent to child. Every parent is guaranteed to to be full constructed at this time. ADJUST is used similarly to BUILD in Moo/se: when you need to "tweak" your object in ways that are hard to cleanly express in attribute.

DESTRUCT

The order of destruction is:

  1. Children destroyed before parents
  2. Destroy instance data before class data
  3. Slots destroyed in reverse order of declaration

Note: class data is not destroyed until global destruction. The order of class data destruction is not guaranteed because the interpreter itself is going away and may not be stable.

Most of the time you won't need to add behavior to the DESTRUCT phase, but if you do:

DESTRUCT ($destruction) {
    if ($destruction->is_global) {
        ...
    }
    else {
        $self->filesystem->unmount;
    }
}

The DESTRUCT phase receives a Cor::Destruction object which currently has one methods, is_global:

class Cor::Destruction {
    has $is_global :reader :new :isa(Bool);
}

The Cor::Destruction object is not created unless there is a target DESTRUCT phaser to pass it to. Thus, Cor::Destruction will never be passed to itself, avoiding an infinite loop.

Example

Here's an example which allows you to change constructor behavior and count how many instances of the class you have:

class Box {
    shared $num_boxes :reader = 0; # shared means class data
    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;
    }

    # called before initialization. No instance variable has a value at this
    # time.
    CONSTRUCT (@args) {
        if ( @args == 1 ) {
            my $num = $args[0];
            @args = map { $_ => $num } qw<height width depth>;
        }
        return @args;
    }

    # called after initialization. 
    # yes, this example is silly
    ADJUST (@args) { # same arguments as CONSTRUCT
        if (exists $ENV{MAX_VOLUME} && $volume > $ENV{MAX_VOLUME}) {
            croak("$volume is too big! Too big! This ain't gonna work!");
        }
        $num_boxes++;
    }

    DESTRUCT($destruct_object) {
        $num_boxes--;
    }
}

With the above, you can create a box and a cube (a box with all sides equal):

say Box->num_boxes;   # 0
my $box  = Box->new( height => 7, width => 3, depth => 42.2 );
my $cube = Box->new(3); 
say Box->num_boxes;   # 2
say $box->num_boxes;  # 2
say $cube->num_boxes; # 2
undef $cube;
say Box->num_boxes;   # 1
say $box->num_boxes;  # 1

Roles

Because phases such as CONSTRUCT and friends aren't methods, roles cannot provide these behaviors. Roles are restricted to requiring and providing methods. If this proves too much of a limitation, we will revisit this in v2.