Reputation: 25117
What is the simplest way to check if invalid arguments are passed to the constructor method new
?
use v6;
unit class Abc;
has Int $.a;
my $new = Abc.new( :b(4) );
Upvotes: 4
Views: 215
Reputation: 34120
TLDR; If you are just worried about someone accidently mis-typing :a(4)
as
:b(4)
, it might be better to just mark $.a
as required.
class ABC {
has Int $.a is required;
}
ABC.new( :b(4) ); # error
# The attribute '$!a' is required, but you did not provide a value for it.
A quick hack would be to add a submethod TWEAK
that makes sure any named values that you don't specify aren't there. It doesn't interfere with the normal workings of new
and BUILD
, so the normal type checks work without having to re-implement them.
class ABC {
has Int $.a;
submethod TWEAK (
:a($), # capture :a so the next won't capture it
*% # capture remaining named
() # make sure it is empty
) {}
}
A slightly more involved (but still hacky) way that should continue to work for subclasses, and that doesn't need to be updated with the addition of more attributes:
class ABC {
has Int $.a;
submethod TWEAK (
*%_ # capture all named
) {
# get the attributes that are known about
# (should remove any private attributes from this list)
my \accepted = say self.^attributes».name».subst(/.'!'/,'');
# ignore any that we know about
%_{|accepted}:delete;
# fail if there are any left
fail "invalid attributes %_.keys.List()" if %_
}
}
Upvotes: 7
Reputation: 32489
new
Add this method declaration to your class:
method new ( :$a is required ) { callsame }
The :$a
binds to a named argument named a
(i.e. a key/value pair whose key is 'a'
, eg. a => 4
).
The is required
that follows the parameter name makes the a
argument mandatory.
Now calls that do not pass a named argument named a
will be rejected:
Abc.new( :b(4) ) ; # Error: Required named parameter 'a' not passed
The body of your new new
method calls callsame
. It calls the new
that your class inherits, namely Mu
's new
. This routine walks through your class and its ancestors initializing attributes whose names correspond to named arguments:
Abc.new( :a(4) ) ; # OK. Initializes new object's `$!a` to `4`
Abc.new( :a(4) ).a.say ; # Displays `4`
UPD: See Brad's answer for a simpler approach that just adds the is required
directly to the existing attribute declaration in the class:
has Int $.a is required; # now there's no need for a custom `new`
ABC.new( :b(4) ); # The attribute '$!a' is required...
Note the shift in the error message from Required named parameter 'a' not passed
to attribute '$!a' is required...
. This reflects the shift from adding a custom new
with a required routine parameter that then gets automatically bound to the attribute, to just adding the is required
directly to the attribute.
If that's not enough, read on.
new
Accepts any and all named arguments (pairs). It then passes them on to other routines that are automatically called during object construction. You passed the named argument :b(4)
in your example code. It was accepted and passed on.
Rejects any and all positional arguments (not pairs).
The new
call in your original code was accepted because you only passed a named argument, so no positional arguments, thus satisfying the argument validation directly done by the default new
.
method new ( :$a!, *%rest ) { %rest and die 'nope'; callsame }
The *%rest
"slurps" all named arguments other than one named a
into a slurpy hash. If it is not empty then the die
triggers.
See also timotimo's answer.
It's almost always both simpler and better to use named parameters/arguments to automatically initialize corresponding object attributes as shown above. This is especially true if you want to make it easy for folk to both inherit from your class and add attributes they also want initialized during new
.
But if you wish to over-rule this rule-of-thumb, you can require one or more positional parameters/arguments and call methods on the new object to initialize it from the passed argument(s).
Perhaps the simplest way to do this is to alter the attribute so that it's publicly writable:
has Int $.a is rw;
and add something like:
method new ( $a ) { with callwith() { .a = $a; $_ } }
The callwith()
routine calls the inherited new
with no arguments, positional or named. The default (Mu
) new
returns a newly constructed object. .a = $a
sets its a
attribute and the $_
returns it. So:
my $new = Abc.new( 4 );
say $new.a ; # Displays `4`
If you don't want a publicly writable attribute, then leave the has
as it was and instead write something like:
method !a ( $a ) { $!a = $a } # Methods starting `!` are private
method new ( $a ) { with callwith() { $_!a($a); $_ } }
Upvotes: 5
Reputation: 4329
The ClassX::StrictConstructor
module should help. Install it with zef install ClassX::StrictConstructor
and use it like so:
use ClassX::StrictConstructor;
class Stricter does ClassX::StrictConstructor {
has $.thing;
}
throws-like { Stricter.new(thing => 1, bad => 99) }, X::UnknownAttribute;
Upvotes: 9