RedGrittyBrick
RedGrittyBrick

Reputation: 4002

idioms for named parameters in object constructors in Perl

In Perl, if I want to use named parameters in an object constructor, my code seems a bit clumsy if I wish to have some validation.

sub new {

   my $class = shift;
   my $self = {};

   my %args = @_;
   foreach my $argname (keys %args) {
     if    ($argname eq 'FOO') { $self->{$argname} = $args{$argname}; }
     elsif ($argname eq 'BAR') { $self->{$argname} = $args{$argname}; }
     elsif ($argname eq 'BAZ') { $self->{$argname} = $args{$argname}; }
     …
     else                      { die "illegal argument $argname\n"; }
   }

   bless $self;
   return $self;
}

Firstly it seems a bit clumsy to have a temporary hash (%args). Secondly the whole if chain seems verbose and tedious.

The latter can be simplified to

  if ('-FOO-BAR-BAZ-'=~m/-$argname-/) { $self->{$argname} = $args{$argname} }
  else { die "..."; }

but I imagine this can be improved.

If I need to check values, the if … elsif chain is still necessary?

I've searched a little but cannot find a better idiom. Is there one (other than using a Perl OO framework of some sort)

Upvotes: 1

Views: 633

Answers (5)

RedGrittyBrick
RedGrittyBrick

Reputation: 4002

For completeness I'm adding this answer (to my own question) describing what I'm actually going to do, which is based on elements from several answers.

sub new {
  my $package = shift;

  # 1. validate argument names
  my %args = @_;
  my $valid = '^FOO|BAR|BAZ$';
  for (keys %args) { die "invalid arg $_\n" unless /$valid/; }

  # 2. construct instance from arguments
  return bless { @_ };
}

I've accepted Sebastian's answer although I'm not using Params::Validate yet.

Notes:

  • I'm deploying to a server that has Perl 5.8 (really) but not Params::Validate. I have reasons for not yet pushing for the upgrades to 5.10.x etc.

  • For my specific circumstance the above strikes a good balance between brevity and readability. I can later add more validation without too much refactoring.

  • This compensates for one of the advantages of a getter/setter or accessor style methods for setting parameters (compiler catches typos in parameter name as that is the method name) whilst being more concise.

For other people the above will not apply, so I have accepted Sebastian's answer which I feel is the best one in general (YMMV).

Upvotes: 0

Sebastian Stumpf
Sebastian Stumpf

Reputation: 2791

I found myself constantly writing unnecessary code which checked the given parameters. But then I discovered Params::Validate. It is easy to use and if the validation fails it provides very clear and user-friendly error messages. Covering all possible combinations of parameters and their error messages is a tedious task. I prefer this way instead:

use Params::Validate qw/:all/;
sub new {
    my $pkg = shift;
    validate(
        @_, {
            foo => { type => SCALAR | ARRAYREF },
            bar => { type => SCALAR, optional => 1},
            baz => { type => ARRAYREF, default => ['value'] },
            quux => { isa => 'CGI' }
        }
    );

    return bless { @_ }, $pkg;
}

And later this code

MyApp::Something->new(
    foo => 123,
    bbr => 'typo',
    quux => CGI->new()
);

becomes:

The following parameter was passed in the call to MyApp::Something::new but was not listed in the validation options: bbr
 at test.pl line 14.
    MyApp::Something::new(undef, 'foo', 123, 'bbr', 'typo', 'quux', 'CGI=HASH(0x7fd4fa1857e0)') called at test.pl line 27

Upvotes: 8

tuxuday
tuxuday

Reputation: 3037

Warning! Untested code.

Check for valid keys.

die "invalid args" if grep { ! /^FOO|BAR|BAZ$/ } keys %args;

Store %args.

$self->{$_} = $args{$_} foreach(keys %args);

Upvotes: 1

TLP
TLP

Reputation: 67908

You can use smart matching

my @validkeys = qw(FOO BAR BAZ);
if ($argname ~~ @validkeys) {     # smart matching
    $self->{$argname} = $args{$argname};
} else { die ... } 

If you don't like the obscurity of the smart match operator you can swing together a regex

my $rx = '^' . join("|", @validkeys) . '$';
if ($argname =~ /$rx/) { ...

Upvotes: 2

Hachi
Hachi

Reputation: 3289

for validation you can define a hash of all legal argument and then just test, if the keys are in it or not

for example:

my %legal = ('FOO' => 1, 'BAR' => 1, 'BAZ' => 1);
my %args = @_;
foreach my $argname (keys %args) {
    if(exists $legal{$argname}) { $self->{$argname} = $args{$argname}; }
    else { die "illegal argument $argname\n"; }
}

about the clumsyness: well that's the to do it in perl it can use hashes efficiently and the hash literals are readable

Upvotes: 1

Related Questions