-
Notifications
You must be signed in to change notification settings - Fork 19
Class Phases
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
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:
-
CONSTRUCT
is called before attribute initialization. It receives the arguments passed to the constructor. -
NEW
takes the arguments from every construct method and assigns the data to the instance variables. -
ADJUST
is called after attribute initialization but before the instance is returned to the calling code. It receives the same arguments asCONSTRUCT
-
DESTRUCT
is called at object destruction.
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.
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 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
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.
The order of destruction is:
- Children destroyed before parents
- Destroy instance data before class data
- 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.
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
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.
Corinna—Bringing Modern OO to Perl