1

I have this block of Perl code that I want to simplify. It's part of a subroutine, where a series of arguments are added to values to a hash of hashes.

$user{$account_num}{'item0'} += $item0;
$user{$account_num}{'item1'} += $item1;
$user{$account_num}{'item2'} += $item2;
$user{$account_num}{'item3'} += $item3;
$user{$account_num}{'item4'} += $item4;
$user{$account_num}{'item5'} += $item5;
$user{$account_num}{'item6'} += $item6;

You can see that the variable names are the same as the key names. I seem to remember watching a friend do this kind of thing in one line once. Something like:

[looping through input arguments]:
$user{$account_num}{'variable_name'} += $variable_name;

Does anybody know of a way to do this?

John G.
  • 11
  • 1
  • 2
  • 2
    This is a very, very bad code smell. Try using a hash of array references. –  Aug 23 '13 at 17:48
  • 2
    [When you find yourself adding an integer suffix to variable names, think I should have used an array](http://stackoverflow.com/questions/1829922/concatenating-variable-names-in-c). Same principle applies here. Also [Why it's stupid to `use a variable as a variable name'](http://perl.plover.com/varvarname.html). – Sinan Ünür Aug 23 '13 at 17:50
  • An array would be fine, except these aren't my real-life variable names. It was necessary to mask them for this example, and I probably shouldn't have made them look like an array. – John G. Aug 23 '13 at 19:02

6 Answers6

4

The syntax is

for my $i (0..6) {
   my $variable_name = "item$i";
   $user{$account_num}{$variable_name} += $$variable_name;
}

but it assumes $item0 and such are package variables, and its use goes against the fundamental principles of software development.


You should be using an array.

$user{$account_num}{'item0'} += $items[0];
$user{$account_num}{'item1'} += $items[1];
$user{$account_num}{'item2'} += $items[2];
$user{$account_num}{'item3'} += $items[3];
$user{$account_num}{'item4'} += $items[4];
$user{$account_num}{'item5'} += $items[5];
$user{$account_num}{'item6'} += $items[6];

which allows you to use

for my $i (0..$#items) {
    $user{$account_num}{"item$i"} += $items[$i];
}
ikegami
  • 367,544
  • 15
  • 269
  • 518
4

You're over thinking it.

I can tell because I do it all of the time:

Me: If I make a device that can vibrate the socket with the right frequency, I can cause that bulb to screw and unscrew. I can then attach the device to the socket...

My Son: Dad, why don't you just use the light switch?

If you have a bunch of items called item1, item2, item3, etc. Why not make this an array of items? Then you would have $item[0], $item[1], etc. That would greatly simplify your code:

for item ( @items ) {
    $user{$account_name}->[$item] += $items[item];
}

Maybe you were using item1 and item2 as stand-ins to the actual variable names. Maybe they're $widget and $left_handed_smoke_shifter. In that case, you can make it a hash of items, so you would have $item{left_handed_smoke_shifter} and $item{widget}. Then, your loop would look something like this:

for my $item ( keys %items ) {
    $user{$account_name}->{$item} += $items{$item};
}

Simple, easy to understand and maintain.

However, if you really, truly insist that you need to be able to use variable names the way you insist, use eval:

$user{$account_name}->{$varible_name} = eval '$' . "$variable_name";

I just sell lengths of ropes that are long enough for someone to hang themselves with it. What they actually do with it is none of my business.

Community
  • 1
  • 1
David W.
  • 105,218
  • 39
  • 216
  • 337
  • Yes, the variable names were indeed stand-ins. I should have been creative and chosen less array-like stand-ins. I think I will make an array of my key names, since they are used throughout the code, and then loop through the argument inputs, in order, since they are already passed to the subroutine in an array. Thanks! – John G. Aug 23 '13 at 19:18
  • I'm all for paring code down as much as possible, but not if it requires using unsustainable programming techniques. So, I don't think I'll pursue EVAL. – John G. Aug 23 '13 at 19:20
  • @JohnG. Thank you for not choosing `eval`. The hash idea works though. Look at this [Perl tutorial on Object Oriented programming](http://perldoc.perl.org/perlootut.html). When you start using more complex structures than simple hashes and arrays, you should start thinking about OO programming techniques. It's really pretty simple and can save you from a ton of hurt. – David W. Aug 23 '13 at 19:31
  • You don't actually need to use eval, `$$variable_name` would also work, assuming the value of `$variable_name` was a simple string. You would need a `no strict 'refs'` before it. ( You also didn't need to put the variable in quotes `.` would have stringified it for you ) – Brad Gilbert Aug 24 '13 at 00:36
1

The problem with the above code is you want to use the name of the variable as the key, which could look like this:

for my $item ( @items ) {
   $user{$account_num}{$item} = $item
}

the problem being that $items value is going to be used as the key, not its name.

Possible solutions:

This would be greatly simplified if you also had a list of the key names:

my @keys = qw(item0 item1 item2 item3 item4 item5 item6);

Now assuming you actually have all of these items coming into your subroutine, you can easily loop

# 1
sub update_items {
   my @items = @_;

   for my $k ( @keys ) { 
      my $item = shift @items;          # take an item from the front
      $user{$account_num}{$k} += $item; # update
   }
}

OR you could use a technique called hash-slicing which takes a hash in list context and assigns a list of values to a list of keys:

# 2
sub update_items {
   my @items = @_;

   # update all keys of @keys with all values of @items
   @user{$account_num}{@keys} += @items;
}
Hunter McMillen
  • 59,865
  • 24
  • 119
  • 170
  • Do *compound assignment operators* work with slices like that? In my quick testing, `@h{@keys} += @values` appears merely to put a scalar context on `@values` and add that number to the last element of the slice (i.e., `$h{ $key[-1] } += scalar(@values)`). – pilcrow Aug 23 '13 at 18:56
  • weird, I had assumed that any assignment operator could be used since the left side has to be calculated first anyway. I guess not though, my mistake. – Hunter McMillen Aug 23 '13 at 18:57
1

Here's a simple example in the Perl debugger:

main::(-e:1):   0
  DB<1> @a=('v1', 'v2', 'v3', 'v4')

  DB<2> x @a
0  'v1'
1  'v2'
2  'v3'
3  'v4'
  DB<3> $v1=10
  DB<4> $v2=20
  DB<5> $v3=30
  DB<6> $v4=40

  DB<7> for (@a) { $hash{$_} += $$_ }

  DB<8> x %hash
0  'v4'
1  40
2  'v1'
3  10
4  'v2'
5  20
6  'v3'
7  30
  DB<10>

The key is $$_, which uses variable indirection to get the value of a variable whose name is contained in a variable, in this case $_.

Jim Garrison
  • 85,615
  • 20
  • 155
  • 190
  • +1 I forgot that you could do this. – Hunter McMillen Aug 23 '13 at 17:35
  • 2
    [Why it's stupid to `use a variable as a variable name](http://perl.plover.com/varvarname.html) – ikegami Aug 23 '13 at 17:57
  • If you think about it abstractly, variable indirection is no different from a C/C++ pointer, and the extra `$` is the de-reference operator. Not that I would use this pattern, just answering the OP's question. – Jim Garrison Aug 23 '13 at 18:09
  • 1
    @Jim Garrison, Not so. While references are similar C and C++ pointers, and while indirection via references is perfectly fine, we're not talking about either. The topic at hand is symbolic references, something that has no peer in C and C++ since they don't have a run-time accessible symbol table. – ikegami Aug 23 '13 at 19:29
1
#!/usr/bin/env perl

use strict;
use warnings;

use Data::Dumper;
$Data::Dumper::Indent = 0;

my %user = (
    '0022334401' => [ map int(1000 * rand), 1 .. 7 ],
    '1177459393' => [ map int(1000 * rand), 1 .. 7 ],
);

my $account_id = '0022334401';

print "Before:\n", Dumper($user{$account_id}) ,"\n";

my @vals = (11, 22, 33, 44, 55, 66, 77);

# done setting up. now, update each value for $user{$account_id}
# by adding the value at the corresponding index in @vals

$user{$account_id}[$_] += $vals[$_] for 0 .. $#vals;

print "After:\n", Dumper($user{$account_id}), "\n";

Sample output:

Before:
$VAR1 = [645,553,864,165,463,455,906];
After:
$VAR1 = [656,575,897,209,518,521,983];
Sinan Ünür
  • 116,958
  • 15
  • 196
  • 339
0

The tidiest way is with eval. Like this

$user{$account_num}{$_} += eval "\$$_" for 'item0' .. 'item6'

but remember what others have said, that you shouldn't be doing this sort of thing. Refactor the code as soon as it is appropriate.

Borodin
  • 126,100
  • 9
  • 70
  • 144