Skip to content

Constructors

Ovid edited this page May 17, 2020 · 23 revisions

Click here to provide feedback.

Version 2

Thanks to Damian Conway for pointing out some problems with the first constructor approach.

  • BUILDARGS and BUILD have been renamed to CONSTRUCT and ADJUST, respectively, because their semantics don't entirely match either Moo/se or Raku.
  • Removed references to method overloading
  • Removed references to positional constructor arguments

The Constructor

This is a first pass at declaring how constructors work, taking into consideration the deconstructing constructors page listing current limitations with the attributes. Specifically:

  1. You cannot declare constructor arguments unless they have a corresponding slot.
  2. Constructor attributes are were not composable.
  3. We don't have first-class constructors in Perl.

Constructor attributes

Slots are defined via has $varname;. This does nothing outside of declaring the slot. Nothing at all. Instead, if we wish to specify arguments for the constructor, we use the :new(...) syntax. Here are the options:

Attribute Meaning
:new Must be passed to the constructor
:new(optional) May be passed to the constructor

Not passing any of these to the constructor means the value may not be passed to the constructor, but the CONSTRUCT method can compensate for that.

Constructor Behavior

Nothing is set in stone.

  1. All Cor constructors take a list of named arguments.
  2. A single argument hashref, as allowed in Moose, is not allowed unless internally, Cor deliberately coerces that to a list.
  3. The constructor is named new.
  4. Slots (instance variables) are assigned values from constructor arguments according to their corresponding :new(...) attributes. This is done internally via the NEW method.
  5. Unknown attributes passed to the constructor are fatal unless a CONSTRUCT method is supplied
  6. Before the constructor is called, we call an implicit CONSTRUCT method from the UNIVERSAL::Cor class. This disables explicit calling of the NEW method.
  7. After the constructor is called, we call an implicit ADJUST method from the UNIVERSAL::Cor class
  8. Every CONSTRUCT and ADJUST method has an implicit $self->next::method(%args) as the first line (and yes, Damian, it's C3) :)
  9. The :new(...) attributes are are ignored if a CONSTRUCT method is called, unless $class->NEW(%args) is explicitly called.

Regarding point 2 above, if we allow the hashref syntax, we coerce to a list to avoid having the poor object implementer writing unholy incantations such as this one that I commonly invoke, knowing full well that it's wrong:

my %args = 1 == @_ ? $_[0]->%* : @_;

Explanation

Let's consider this Box class.

class Box {
    has ($width, $height, $depth) :new :reader :isa(Num);
    has $volume :reader = $width * $height * $depth; # this works because defaults are lazy
}

my $box = Box->new(height => 7, width => 7, depth => 7);
say $box->volume;   # 343

Internally, Cor says "we don't have a ADJUST method, so we call the &UNIVERSAL::Cor::CONSTRUCT method which might look like this pseudo-code(it will be magic):

# note: there are probably seriously missing corner cases here
# and I'm sure this is wrong. Please throw stones.

method CONSTRUCT (%args) {
    if ( called directly and not from a child's CONSTRUCT method ) {
        $self->NEW(%args);
    }
}

method NEW (%args) {
    foreach my $key (%args) {
        # validated against :new
        # throw exception if slot for $key doesn't have :new or :new(optional)
        # assign $args{$key} to a slot. Any slot assignment checks :isa
    }
    foreach $attr (:new required attributes) {
        # throw exception if missing
    }
}

I'll call the NEW method which will set the attribute values according to the :new(...) attributes."

But that box is a cube, so maybe we want shorthand for Box->new(length => 7)? We would do this:

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

    method CONSTRUCT (%arg_for) {
        if ( exists $arg_for{length} ) {
            # no call to ->NEW, but we still check the types via `:isa(Num)`
            # because slot assignment will always check the type if it exists
            $width = $height = $depth = $arg_for{length};
        }
        else {
            $self->NEW(%arg_for);
        }
    }
}

my $box = Box->new(length => 7);
say $box->volume;   # 343

In the above, we implicitly inherit from UNIVERSAL::Cor and the CONSTRUCT method implicitly calls $self->next::method(%arg_for) as the first line. However, the parent method sees that it hasn't been called directly, so it trusts the child to Do The Right Thing (parents can be so naïve).

Alternatively, and probably safer, you could do this:

    method CONSTRUCT (%arg_for) {
        if ( exists $arg_for{length} ) {
             $arg_for{width} = $arg_for{height} = $arg_for{depth} = delete $arg_for{length};
        }
        $self->NEW(%arg_for);
    }

This approach does have a limitation that Box->new(7) isn't possible. Instead, create a subroutine that calls the constructor for you:

sub new_cube ($class, $length) {
    $class->new(length => $length);
}

Yes, it's a touch clumsy, but it works.

Also, note that in the above, CONSTRUCT has a $self and not a $class variable. This is because Cor has no way of directly declaring class methods. The modifier shared may be appropriate here, since this method is shared across classes.

shared method new_cube ($length) {
    $class->new(length => $length);
}

The above has the advantage of allowing the MOP to understand that this is a method and not a subroutine.

Suggestions welcome.

Clone this wiki locally