sid_com
sid_com

Reputation: 25117

Perl 6: how to check `new` for invalid arguments?

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

Answers (3)

Brad Gilbert
Brad Gilbert

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

raiph
raiph

Reputation: 32489

Write a custom 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.

The default 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.

Rejecting unknown named arguments (as well as any positional arguments)

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.

Requiring positional arguments instead of named

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

timotimo
timotimo

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

Related Questions