CodeGorilla
CodeGorilla

Reputation: 913

In perl, can I dynamically create variables within a subroutine?

Background

In the code I'm writing, I'm passing data into methods using a hash-ref (see note [1]).

This, unfortunately, leads to a lot of repetitive code:

sub thing {
  my ($self, $params) = @_;

  my ($foo, $bar, $baz, $biff,);
  if ( exists $params->{foo} && $params->{foo} ) {
     $foo = $params->{foo};
  }
  # repeat for `bar`, `baz`, `biff`

  ## rest of function ##
}

(and duplicate in every function with parameters)

What I want to do

What would be far easier is to define a list of parameters, and then iterate of that list, creating both the variables and setting them to a value if needed.

So to test this, I tried:

my $params = { x => 1, y => 2};
my @params = qw(x y z a b c);

gno strict 'refs';
rep( ${$_}, @params );
use strict 'refs';

foreach my $p (@params) {
  if ( exists $params->{$p} && $params->{$p} ) {
    ${$p} = $params->{$p};
  }
}
print "x:$x, y:$y, z:$z, a:$a, b:$b, c:$c\n"

which gives me the following error:

Global symbol "$x" requires explicit package name at ./test.pl line 20.
Global symbol "$y" requires explicit package name at ./test.pl line 20.
Global symbol "$z" requires explicit package name at ./test.pl line 20.
Global symbol "$c" requires explicit package name at ./test.pl line 20.

Can I do this dynamic variable creation thing? (and if so, how?)


[1] By using a hash to pass data in, I gain in many ways:

  1. There is a clear indication of What each item of data is
  2. The ORDER of the pieces of data is no longer important
  3. I can miss one or more pieces of data, and I don't need to add in random undef values
  4. I'm passing less data: 1 scalar (a reference) rather than multiple scalars
  5. (I accept the danger of functions being able to change the parent's data, rather that mucking around with a copy of it...)

Upvotes: 1

Views: 1091

Answers (5)

Ruud H.G. van Tol
Ruud H.G. van Tol

Reputation: 1

perl -Mstrict -MData::Dumper -wE'
  {package Data::Dumper;our($Indent,$Sortkeys,$Terse,$Useqq)=(1)x4}
  my @aok = qw( x  y  z  a  b  c );
  my %dft = ( a => -1 );
  say "- - - -";
  my $arg = { x => 1, y => 2, foo => 42 };
  $arg = { %dft, %$arg };
  say "arg: ", Dumper($arg);

  my %var;                     
  @var{ @aok } = @$arg{ @aok };
  say "var: ", Dumper(\%var);

  my %aok = map { $_ => 1 } @aok;
  my @huh = grep !$aok{$_}, sort keys %$arg;
  @huh and say "huh: ", Dumper(\@huh);
'
- - - -
arg: {
  "a" => -1,
  "foo" => 42,
  "x" => 1,
  "y" => 2
}

var: {
  "a" => -1,
  "b" => undef,
  "c" => undef,
  "x" => 1,
  "y" => 2,
  "z" => undef
}

huh: [
  "foo"
]

Upvotes: 0

CodeGorilla
CodeGorilla

Reputation: 913

With thanks to Dave Cross & others - the following test works:

#!/usr/bin/perl 
use strict;
use warnings;
use English qw( -no_match_vars ) ;
use Carp;
use Data::Dumper;

my $params = { x => 1, y => 2, z => 0};
my @params = qw(x y z a b c);

my %var;

foreach my $p (@params) {
  if ( exists $params->{$p} ) {
    $var{$p} = $params->{$p};
  } else {
    $var{$p} = undef;
  }
}

print Dumper \%var;

This gives me %var with all desired parameters (as listed in @params, with the ones that are not passed in (ie, not in the $params hashref) created with an undef value.

Thus I can confidently test for value and truth, without worrying about existence.

Thank you all.

Upvotes: 1

Dave Cross
Dave Cross

Reputation: 69244

Yes, you can do this in Perl. But it's a terrible idea for all of the reasons explained by Mark Dominus in these three articles.

It's a far better idea to store these values in a hash.

#!/usr/bin/perl

use strict;
use warnings;

my $params = { x => 1, y => 2};
my @params = qw(x y z a b c);

my %var;

foreach my $p (@params) {

  # You need to take care exactly what you want in this
  # logical statement. The options are:
  # 1/ $p exists in the hash
  #    exists $params->{$p}
  # 2/ $p exists in the hash and has a defined value
  #    defined $params->{$p}
  # 3/ $p exists in the hash and has a true value
  #    $params->{$p}
  # I think the first option is most likely. The last one has
  # good chance of introducing subtle bugs.

  if ( exists $params->{$p} ) {
    $var{$p} = $params->{$p};
  }
}

print join ', ', map { "$_: " . ($var{$_} // 'undef') } @params;
print "\n";

Upvotes: 5

stevieb
stevieb

Reputation: 9296

It's a really bad idea to use symbolic references like this... hashes pretty well completely eliminate the need for this.

use warnings;
use strict;

my $params = { x => 1, y => 2, foo => 3, };

thing($params);

sub thing {
    my $params = shift;
    my $foo;
    if (defined $params->{foo}){
        $foo = $params->{foo};
    }
    print $foo;
}

You can also pass in a hash itself directly (whether it be pre-created, or passed inline to the sub. If pre-created, the sub will operate on a copy).

thing(foo => 1, x => 2);

sub thing {
    my %params = @_;
    print $params{foo} if defined $params{foo};
}

Upvotes: 2

Hellmar Becker
Hellmar Becker

Reputation: 2972

I did this using soft references:

#!perl

no strict "refs";

my %vars = ( x => 1, y => 2 );

for my $k ( keys %vars ) {
  $$k = $vars{$k};
}

print $x, $y;

But there's a reason why the recommended settings (use strict; use warnings;) prevent this kind of pattern. It is easy to shoot yourself in the foot with it.

Upvotes: 0

Related Questions