Skip to content

Constructors

Ovid edited this page Mar 22, 2020 · 23 revisions

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 not composable.
  3. We don't have first-class constructors in Perl.

What I would like is additional constructors similar to the Java approach:

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

    Box ($length) {
        # no need to declare $length :isa(Num) because the slot assignment
        # will check that
        $width = $height = $depth = $length;
    }
}

In the above, we would still have Box->new(%kv_pairs) (implicit via the :new attribute), but we'd also have Box->new($length).

But that ain't gonna happen. So let's take another swing at this. The following is based on my willful misunderstanding of a lovely email sent to me by Damian Conway. He made a lot of sense, so let's see how badly I can get this wrong.

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(required) Must be passed to the constructor
:new(optional) May be passed to the constructor
:new(forbidden) Must not be passed to the constructor
:new Same as :new(required)

Not passing any of these to the constructor is the same as :new(forbidden), but the BUILDARGS method can compensate for that.

Constructor Behavior

Nothing is set in stone. In particular, I don't like the BUILDARGS or BUILD names, but they're common in Moose and generally understood.

  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 BUILDARGS method is supplied
  6. Before the constructor is called, we call an implicit BUILDARGS method from the UNIVERSAL::Cor class. This disables explicit calling of the NEW method.
  7. After the constructor is called, we call an implicit BUILD method from the UNIVERSAL::Cor class
  8. Every BUILDARGS and BUILD method has an implicit $self->next::method(%args) as the first line
  9. The :new(...) attributes are are ignored if a BUILDARGS 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 BUILD method, so we call the &UNIVERSAL::Cor::BUILDARGS method which might look like this pseudo-code(it will be magic):

# note: there are probably seriously missing corner cases here

method BUILDARGS (%args) {
    if ( called directly and not from a child's BUILDARGS 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 BUILDARGS (%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 BUILDARGS 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 BUILDARGS (%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, BUILDARGS has a $self and not a $class variable. This is because Cor has no way of directly declaring class methods. Maybe reusing the class keyword?

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

Also, reread my desired syntax and compare to the above. If this was possible, we might be able to do away with BUILDARGS and BUILD entirely.

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

    Box ($length) {
        $width = $height = $depth = $length;
    }
}
Clone this wiki locally