-
Notifications
You must be signed in to change notification settings - Fork 19
Constructors
Click here to provide feedback.
Thanks to Damian Conway for pointing out some problems with the first constructor approach.
-
BUILDARGS
andBUILD
have been renamed toCONSTRUCT
andADJUST
, respectively, because their semantics don't entirely match either Moo/se or Raku. - Removed references to method overloading
- Removed references to positional constructor arguments
This is a first pass at declaring how constructors work, taking into consideration the deconstructing constructors page listing current limitations with the attributes. Specifically:
- You cannot declare constructor arguments unless they have a corresponding slot.
- Constructor attributes
arewere not composable. - We don't have first-class constructors in Perl.
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.
Nothing is set in stone.
- All Cor constructors take a list of named arguments.
- A single argument hashref, as allowed in Moose, is not allowed unless internally, Cor deliberately coerces that to a list.
- The constructor is named
new
. - Slots (instance variables) are assigned values from constructor arguments according to their corresponding
:new(...)
attributes. This is done internally via theNEW
method. - Unknown attributes passed to the constructor are fatal unless a
CONSTRUCT
method is supplied -
Before the constructor is called, we call an implicit
CONSTRUCT
method from theUNIVERSAL::Cor
class. This disables explicit calling of theNEW
method. -
After the constructor is called, we call an implicit
ADJUST
method from theUNIVERSAL::Cor
class - Every
CONSTRUCT
andADJUST
method has an implicit$self->next::method(%args)
as the first line (and yes, Damian, it's C3) :) - The
:new(...)
attributes are are ignored if aCONSTRUCT
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]->%* : @_;
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.
Corinna—Bringing Modern OO to Perl