mswanberg
mswanberg

Reputation: 1285

How can I iterate over an array with foreach without changing the address of the loop variable?

When I run a simple bit of code like this:

my @arr=(1..5);
my $x;
foreach $x (@arr) {
    $x+=10;
}
print "@arr";

The result is "11 12 13 14 15" because $x "becomes" each element in the @arr array in the foreach. Well enough.

But here's my thing... not so much a problem (the solution is easy, but inelegant, and I want my perl to be as elegant as possible).

I wrote a tie module for dealing with COBOL data. It takes a copybook, parses the fields, and then attaches that to a scalar/string so that access to/from the tied hash will return/set values in the string. It works wonderfully.

my %h,$rec;
my $cb=<<END;
       01 CH-RECORD.
          05 JOB-NUM PIC X.
          05 FILLER  PIC X(76).
          05 REC-TYPE PIC X(2).
END
tie %h, 'COBOLDataTie',$cb,\$rec; #tie the hash to the record via the copybook

From there, I can move a COBOL record to $rec and access the COBOL fields with the %h hash.

Again, this works perfectly. But the problem comes when I want to iterate over, say, an array of COBOL records. So if after the above code I had something akin to:

foreach $rec (@arr) {
    print "Job is ",$h{'JOB-NUM'},"\n";
}

it won't work because the foreach actually changes the location of $rec, which breaks the tie on it. I end up having to do something like this:

foreach (@arr) {
    $rec=$_;
    print "Job is ",$h{'JOB-NUM'},"\n";
}

Is there any way I can do the "foreach $rec (@arr)" and not break my tied hash?

(And before anyone says, yes I know this begs for a nice object-oriented solution... some day I'll get to that; I just have to find some time first)

EPILOGUE: I revised the TieHash code to, instead of pointing to an external record, it intercepts "special" keys for the hash, among which is 'record'. So when I assign a record string to $h{'record'} it's the same as loading $rec in the example above. This is a far better solution, more self-contained. It also exposes a more OOP-like interface.

Upvotes: 2

Views: 474

Answers (3)

mswanberg
mswanberg

Reputation: 1285

It seems like the best way is to do something like:

for (my $i=0;($rec=$arr[$i], $i<@arr);$i++) {

Not exactly the elegance I was hoping for, but it seems to work.

Upvotes: 0

mob
mob

Reputation: 118595

This is a subtle point and easy to overlook in the documentation, but the loop variable in a foreach variable is always a new variable, and has nothing to do with any lexical or package variables with the same name anywhere else in the program.

From perlsyn:

Foreach Loops

The "foreach" loop iterates over a normal list value and sets the variable VAR to be each element of the list in turn. If the variable is preceded with the keyword "my", then it is lexically scoped, and is therefore visible only within the loop. Otherwise, the variable is implicitly local to the loop and regains its former value upon exiting the loop. If the variable was previously declared with "my", it uses that variable instead of the global one, but it's still localized to the loop. This implicit localization occurs only in a "foreach" loop.

(emphasis added). That is, the $rec in line 3 of this little script has nothing to do with the $rec declared in line 1

1: my $rec = 'foo';
2: print $rec;                       # 'foo'
3: foreach $rec (@some_list) {
4:     print $rec;                   # something else
5: }
6: print $rec;                       # 'foo' again

So if you want to use \$rec to influence behavior of your tied hash (though there are certainly other ways to do it), then you are doing the right thing to use a different loop variable and assign $rec to it inside the loop.

Upvotes: 2

ikegami
ikegami

Reputation: 385655

The interface you decided to create is "assign to $rec, then access fields via %h". As such, that's exactly what you need to do.

for (@arr) {
    $rec = $_;
    print "Job is $h{'JOB-NUM'}\n";
}

Sure it looks weird, but that's beause it is weird. This would make more reasonable:

for (@arr) {
    my $h = parse($cb, $_);
    print "Job is $h->{'JOB-NUM'}\n";
}

You can even do it with minimal changes:

sub parse {
    my ($cb, $rec) = @_;
    tie my %h, 'COBOLDataTie', $cb, \$rec; 
    return \%h;
}

Upvotes: 2

Related Questions