tags:

views:

171

answers:

4

Hi all,

I thought I understood map however the following has results that I don't understand. I know why it's happening I just don't know how it is happening.

The problem is that the contents of @array are changing because $_ is being reset during _do_stuff_to_file call. so what is printed is here: \nhere:\n when I expect it to be here: donkie\nhere: kong\n.

Note: This is not tested code. It's just what I remember seeing from lab. Why are the contents of @array changing?

If I set $_ to $f before returning 1 from _some_func. Then the array is still intact.

Here is an example program to illustrate what I am seeing:

my @array = ("donkie", "kong");
map { push @junk, _some_func('blah', $_); } @array;

if (join ('', @junk) !~ /0/)
{   # for example sake this is always true since return 1 from _some_func.
    print map { "here: $_\n"; } @array;
}

sub _some_func
{   # for example sake, lets say $f always exists as a file.
    my $j = shift;
    my $f = shift;
    return 0 if !open(FILE, "< $f");
    close FILE;
    _do_stuff_to_file($f);

    return 1;
}


sub _do_stuff_to_file
{
    my $f = shift;
    open(IN, "< $f");
    open(OUT, "> $f.new");

    while (<IN>)
    {
        print OUT;
    }

    close IN;
    close OUT;
}
+8  A: 

Many functions in Perl use the default variable $_. Among these are map and the readline operator <>. Like foreach, map makes the loop variable an alias for each element of the list it processes. What's happening is that this line:

while (<IN>)

is assigning to $_ while the aliasing of the map is in effect. This is one of the problems with using $_ (or any other global variable) -- strange action at a distance. If you're going to use $_, localize it first:

local $_;
while (<IN>)
...

Alternately, use a lexical variable instead:

while (my $line = <IN>)
Michael Carman
but why does using $_ change the contents of the array? isnt $_ just a variable that is a copy of the current value of the array?
okay I see now, thanks.
+4  A: 

modifying $_ will change your initial array, because $_ is an alias to current element. Your code should look like this:

my @array = ("donkie", "kong");
my @junk=map {_some_func('blah', $_) } @array;

if (join ('', @junk) !~ /0/)
{   # for example sake this is always true since return 1 from _some_func.
    print map { "here: $_\n"; } @array;
}

sub _some_func
{   # for example sake, lets say $f always exists as a file.
    my $j = shift;
    my $f = shift;
    return 0 if !-e $f;
    _do_stuff_to_file($f);
    return 1;
}


sub _do_stuff_to_file
{
    my $f = shift;
    local $_;
    open(IN, "<",$f);
    open(OUT, ">", "$f.new");

    while (<IN>)
    {
        print OUT;
    }

    close IN;
    close OUT;
}

P.S. map returns array with same number of elements (if scalar is returned from block). grep returns only elements for which block is true.

Alexandr Ciornii
The list `map` returns doesn't necessarily have the same number of elements as its input. e.g. `@doubled = map { $_, $_ } (1 .. 5)`
Michael Carman
A: 

I approve the answers from Alexander and Michael: _do_stuff_to_file() is changing the value of $_. As in the context of map $_ is just a name for the storage of the mapped element, the array is changed.

Alexander and Michael propose to change _do_stuff_to_file() so they do not affect the $_ value. This is good practice to local-ize special variables such as $_ to avoid perturbing the outside scope.

Here is an alternative solution that avoid touching that function: "break" the link inside the map block by local-izing before calling the function:

map { my $x=$_; local $_; push @junk, _some_func('blah', $x); } @array;

or more following the common style:

@junk = map { my $x=$_; local $_; _some_func('blah', $x) } @array;
dolmen
Or `my $_;` before the map will cause map to use a lexical $_, not the global. (5.10+)
ysth
@ysth "my $_" will create a new variable lexical to the map block. But it will not affect the _some_func() and _do_stuff_to_file() contexts so this will not fix the problem.
dolmen
Yes, it will fix the problem for *that* map, unless you *want* them to be able to change the aliased values in @array.
ysth
+4  A: 

Most things that set $_ implicitly alias it, so won't cause this problem; the exception is while (<filehandle>). While you can localize $_ (ideally with my $_;), it's better just to never let while implicitly set $_. Do while ( my $line = <filehandle> ) instead. (The special implicit defined() still happens.)

ysth