Be Careful With Foreach

Mar 3, 2008

I ran into an interesting side-effect with the foreach loop in Perl today. I'm surprised that I haven't hit this before, but it may be a subtle enough issue that it only pops up under the right circumstances. Here's a sample program that we'll use as an example:

#!/usr/bin/perl
use strict;
use warnings;

my @array = ("Test NUM", "Line NUM", "Part NUM");

for (my $i=0; $i < 3; $i++)
{
    foreach (@array)
    {
        s/NUM/$i/;
        print "$_\n";
    }
    print "------\n";
}

What should the output for this little script look like? Here's what I assumed it would be:

Test 0
Line 0
Part 0
------
Test 1
Line 1
Part 1
------
Test 2
Line 2
Part 2
------

But here's the actual output:

Test 0
Line 0
Part 0
------
Test 0
Line 0
Part 0
------
Test 0
Line 0
Part 0
------

So what's going on here? Well, it turns out that the foreach construct doesn't act quite like I thought it did. Let's isolate just that loop:

foreach (@array)
{
    s/NUM/$i/;
    print "$_\n";
}

We simply loop over each element of the array, we do a substitution, and we print the result. Pretty simple. Pay attention to the fact that we are storing each iteration through the loop in Perl's global $. The point here is that $ doesn't represent a copy of the array element, it represents the actual array element. From the Programming Perl book (which I highly recommend):

foreach VAR (LIST) {
    ...
}
If LIST consists entirely of assignable values (meaning variables, generally, not enumerated constants), you can modify each of those variables by modifying VAR inside the loop. That's because the foreach loop index variable is an implicit alias for each item in the list that you're looping over.

This is an interesting side effect, which can be unwanted in some cases. As a workaround, I simply created a temporary buffer to operate on in my substitution call:

foreach (@array)
{
    my $temp = $_;
    $temp =~ s/NUM/$i/;
    print "$temp\n";
}

An easy fix to a not-so-obvious problem.

No comments (yet!)

Leave a Comment

Ignore this field:
Never displayed
Leave this blank:
Optional; will not be indexed
Ignore this field:
Both Markdown and a limited set of HTML tags are supported
Leave this empty: