-
Notifications
You must be signed in to change notification settings - Fork 19
Constructors
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 are not composable.
- 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.
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.
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.
- 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
BUILDARGS
method is supplied -
Before the constructor is called, we call an implicit
BUILDARGS
method from theUNIVERSAL::Cor
class. This disables explicit calling of theNEW
method. -
After the constructor is called, we call an implicit
BUILD
method from theUNIVERSAL::Cor
class - Every
BUILDARGS
andBUILD
method has an implicit$self->next::method(%args)
as the first line - The
:new(...)
attributes are are ignored if aBUILDARGS
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 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;
}
}
Corinna—Bringing Modern OO to Perl