2

Usually I have a hash of %valid_opts = (a => 1, b => 2, ...) for each valid option that could be passed to a function and then I iterate to find out if an option was passed that wasn't supported. This saves me from myself when a typo happens and I wonder why something is broken:

sub f
{
   my %opts = @_;
   my %valid_opts = (a => 1, b => 1, c => 1);

   foreach (keys(%opts)) {
      croak "bad option $_" if !$valid_opts{$_};
   }

   # profit
}

Recently I learned about the ~~ smart match operator and wondered if it could simplify the job:

Can I use the ~~ operator to check if %opts contains only keys that exist in %valid_opts without a loop?

I've tried a few combinations of things like keys(%opts) ~~ @valid_opts but I think its comparing to make sure they match exactly. If this is possible, then how would you set it up?

This should be true:

  • @valid_opts = (qw/a b c/)
  • %opts = (a => 1, b => 2)

This should be false:

  • @valid_opts = (qw/a b c/)
  • %opts = (a => 1, b => 2, d => 4)

At the risk that this question is really an XY problem, then is there a better way to check for valid options at the top of a function?

brian d foy
  • 129,424
  • 31
  • 207
  • 592
KJ7LNW
  • 1,437
  • 5
  • 11
  • See also [Params::Validate](https://metacpan.org/pod/Params::Validate), and some other questions [here](https://stackoverflow.com/q/29577085/2173773) and [here](https://stackoverflow.com/q/44065722/2173773) – Håkon Hægland Jun 19 '22 at 10:42
  • 6
    Smartmatch is currently deprecated and (retrospectively) marked as experimental. You shouldn't use it in production code, as it's likely to go away or change functionality at some point in the future. (That hasn't happened yet because while everyone agrees that it has a fundamentally broken design, there has never been a consensus on how to fix it.) – Dave Mitchell Jun 19 '22 at 10:52
  • How can `@valid_opts = (qw/a b c/)` be both true and false? Also, that array is not listed in your code. You might be thinking of a hash slice or some such, or a list from `keys`. – TLP Jun 19 '22 at 10:58
  • As far as XY-problems... why would an unused, unsupported option be a problem? A hash key only exists if you call it into existence. E.g. if you never call `$opts{d}` it will never break your code. If you are calling the options with a "get all" functionality, like `for my $opt (keys %opts)`, then perhaps you should validate the keys there before they are used. – TLP Jun 19 '22 at 11:03
  • 1
    Also, I agree and emphasize Dave's comment above: Smart match is not a good idea. It rarely works well for anything. – TLP Jun 19 '22 at 11:05

1 Answers1

3

There are modules that can do this, and are more powerful. But otherwise, you also can create very easily a function to do this on your own. A full example.

I named the file validation.pl in my example.

#!/usr/bin/env perl
use strict;
use warnings;
use v5.32;
use Carp qw(croak);

# Works Fine
helloA(
    Name     => "David",
    LastName => "Raab",
);

helloB(
    Name     => "David",
    LastName => "Raab",
);

# Throws: Keys [LasstName, Invalid] not supported by main::valid_arguments at ./validation.pl line 19.
helloA(
    Name      => "David",
    LasstName => "Raab",
);

# Would also throw exception.
helloB(
    Name      => "David",
    LasstName => "Raab",
);


# valid_arguments(["Name", "LastName"], @_);
sub valid_arguments {
    my ($valids, %orig) = @_;
    
    # Turns: ["A","B","C"] into {"A" => 1, "B" => 1, "C" => 1}
    my %valids = map { $_ => 1 } @$valids;
    
    # Check all passed arguments
    for my $key (keys %orig) {
        # if they are valid entry
        if ( exists $valids{$key} ) {
            # when true - do nothing
        }
        else {
            # when false - throw error
            local $Carp::CarpLevel = 2;
            my @caller = caller 0;
            croak (sprintf "Key [%s] not supported by %s", $key, $caller[3]);
        }
    }
    
    # returns hash in list context. hashref in scalar context.
    return wantarray ? %orig : \%orig;
}

sub helloA {
    my (%args) = valid_arguments(["Name", "LastName"], @_);
    printf "Hello %s %s\n", $args{Name}, $args{LastName};
}

sub helloB {
    my $args = valid_arguments(["Name", "LastName"], @_);
    printf "Hello %s %s\n", $args->{Name}, $args->{LastName};
}

With caller() you can get information from which other source your function is called. Carp::croak() throws an exception from another perspective. So you get an error-message where a user needs it to fix his error.


EDIT: An extended valid_arguments that checks for all arguments.

# Examples: 
# my %args = valid_arguments(["Name", "LastName"], @_);
# my $args = valid_arguments(["Name", "LastName"], @_);
sub valid_arguments {
    my ($valids, %orig) = @_;
    
    # Turns: ["A","B","C"] into {"A" => 1, "B" => 1, "C" => 1}
    my %valids = map { $_ => 1 } @$valids;
    
    # Get a list of invalid arguments
    my @invalids;
    for my $key ( keys %orig ) {
        push @invalids, $key if not exists $valids{$key};
    }
    
    # Throw error if any invalid exist
    if ( @invalids > 0 ) {
        local $Carp::CarpLevel = 2;
        my $caller = (caller 0)[3];
        
        @invalids == 1 
            ? croak (sprintf "Key [%s] not supported by %s", $invalids[0], $caller)
            : croak (sprintf "Keys [%s] not supported by %s", join(", ", @invalids), $caller);
    }
    
    # return hash in list context. hashref in scalar context.
    return wantarray ? %orig : \%orig;
}

Currently all arguments are optional. How about mandatory? You can add this feature too, but I would use a already develeoped and used module instead.

David Raab
  • 4,433
  • 22
  • 40
  • Thanks, good idea. You said there are modules targeted for this: can you suggest such a module that is well supported and commonly used, or, what would I search for on cpan? – KJ7LNW Jun 19 '22 at 22:49
  • 1
    @KJ7LNW If you use something like `Moose`, you can use `Type::Params` that is part of `Type::Tiny`. You also can use `Params::Validate`. – David Raab Jun 20 '22 at 11:23
  • @zdim Sure, added a version that checks all optional arguments. – David Raab Jun 20 '22 at 12:06