Beefy Boxes and Bandwidth Generously Provided by pair Networks
Perl-Sensitive Sunglasses
 
PerlMonks  

changing array size in foreach loop: is it safe?

by Errto (Vicar)
on Feb 07, 2005 at 17:41 UTC ( #428760=perlquestion: print w/ replies, xml ) Need Help??
Errto has asked for the wisdom of the Perl Monks concerning the following question:

I have some code that iterates through an array but might add elements in the process.
use strict; use warnings; my @foo = qw/a b c d e f g/; foreach my $x (@foo) { push @foo, 'h' if $x eq 'd'; print "$x"; }
As expected, this prints out
abcdefgh
But I just read through the foreach section of perlsyn and I am not at all convinced that this should work. I assume this is not a terribly uncommon thing to do, but is there a guaranteed (meaning non-implementation-dependent) way to do it? Thanks.

Comment on changing array size in foreach loop: is it safe?
Download Code
Re: changing array size in foreach loop: is it safe?
by RazorbladeBidet (Friar) on Feb 07, 2005 at 17:49 UTC
    What it says is:
    If any element of LIST is an lvalue, you can modify it by modifying 
    VAR inside the loop. Conversely, if any element of LIST is NOT an 
    lvalue, any attempt to modify that element will fail. In other 
    words, the foreach loop index variable is an implicit alias 
    for each item in the list that you're looping over.
    
    That's referring to the elements of the list, not the list itself. If you're worried, make another array, add new elements onto that, then combine the arrays when it's done.
Re: changing array size in foreach loop: is it safe?
by Tanktalus (Canon) on Feb 07, 2005 at 17:57 UTC

    First off, while not exactly perfectly worded for this question (is push different from splice?), the foreach documentation in perlsyn says, in part:

    If any part of LIST is an array, "foreach" will get very confus +ed if you add or remove elements within the loop body, for example wi +th "splice". So donít do that.
    I think that says "no".

    Now, as to how to do this: no, I don't know a clean way to do that, either. map doesn't quite do it:

    @foo = map { 'd' eq $_ ? ($_,'h') : $_ } @foo; # puts 'h' right after 'd'
    And using an extra variable doesn't quite help, either:
    my @extras; foreach my $x (@foo) { push @extras, 'h' if $x eq 'd'; print $x; } push @foo, @extras; # didn't print 'h'. But if we add: print foreach @extras; # that works ... but duplicates code.
    Looks like the guaranteed solution is a bit longer:
    { my @extras; foreach my $x (@foo) { push @extras, 'h' if $x eq 'd'; } push @foo, @extras; } foreach my $x (@foo) { print $x; }
    Even that misses it in some cases - for example, if what you're pushing onto @foo may need to be treated itself. In this case, imagine pushing on a random letter which itself may be 'd' - and thus you want to push on an extra random letter each time you get another 'd'. Theoretically this may not ever end, but practically you'll stop getting 'd's eventually.

      If any part of LIST is an array, "foreach" will get very 
      confused if you add or remove elements within the loop 
      body, for example with "splice".   So donít do that.
      
      Isn't this just saying if one is manipulating an element that is itself an array, not the LIST itself? I would think as long as one stuck to scalars, it should be OK, although one would have to be careful.

      Update: This one works... iffy (same ballpark), but works:
      @foo = qw/a b c d e f g/; map{ push @foo, 'h' if /^d$/ } @foo; print @foo, "\n";

      I think it's a case of simply having enough rope to hang yourself.
      Your last description calls for reprocessing @extras until it is empty:
      my @foo = qw/a b c d e f g/; my $sub = 'h'; for (my @extras = @foo; @extras; @extras = my @new_extras) { foreach my $x (@extras) { push @new_extras, $sub++ if $x =~ /[dhij]/; } push @foo, @new_extras; } print for @foo; # Tacks on h when it hits the d, then # i for the h, j for the i, and k for the j # yielding abcdefghijk
      I like how the for replaces the bare block for scoping.

      You can also do it without the @new_extras variable by replacing the foreach loop/push combination with a map:

      my @foo = qw/a b c d e f g/; my $sub = 'h'; for (my @extras = @foo; @extras;) { @extras = map { /[dhij]/ ? $sub++ : () } @extras; push @foo, @extras; } print for @foo;

      Caution: Contents may have been coded under pressure.
Re: changing array size in foreach loop: is it safe?
by hardburn (Abbot) on Feb 07, 2005 at 17:59 UTC

    Elements in for are flattened to a list before processing. The array should be ignored after the initial entry into the loop--it will only process the list it created. It should be safe to add elements to the array, though I would hesitate to rely on this behavior.

    "There is no shame in being self-taught, only in not trying to learn in the first place." -- Atrus, Myst: The Book of D'ni.

      That's not the behavior the OP was seeing, so you definitely can't rely on it. The iterator will process elements that were added on to the end of the array within the loop.

      Caution: Contents may have been coded under pressure.
Re: changing array size in foreach loop: is it safe?
by Fletch (Chancellor) on Feb 07, 2005 at 18:01 UTC

    Yeah that's a definite "don't do that". You should either make a duplicate of the array, or in the case that you're just appending you could iterate over indices instead of the array itself.

Re: changing array size in foreach loop: is it safe?
by Roy Johnson (Monsignor) on Feb 07, 2005 at 18:11 UTC
    I can say that it's not a good idea. Look at what you get when you do this:
    my @foo = qw/a b c d e f g/; for my $x (@foo) { push @foo, 'd' if $x eq 'd'; print $x; }
    The iterator keeps marching through the d's, adding new ones to the end. A better way to do it might be:
    push @foo, map {$_ eq 'd' ? 'd' : ()} @foo; print for @foo;
    I doubt that it's implmentation-dependent. The iterator is stepping through the array, and by the time it gets to the end of the array, there's another element there to step through.

    Still, it's not a nice trick to play. Here's an even more diabolical one:

    for (@foo) { print; shift(@foo) if $_ eq 'c'; } print "\n@foo\n";

    Caution: Contents may have been coded under pressure.
Re: changing array size in foreach loop: is it safe?
by blahblahblah (Priest) on Feb 08, 2005 at 05:02 UTC
    I think a clean and clear way to do this is with a while loop, maintaining your own index. Something like this:
    my $i = 0; while ($i <= $#foo) { push @foo, 'h' if $foo[$i] eq 'd'; print $foo[$i]; $i++; }
      It occurred to me that in my real code I don't actually need the value of @foo afterwards, so I should just use shift:
      while (@foo) { my $x = shift @foo; push @foo, 'h' if whatever(); }

Log In?
Username:
Password:

What's my password?
Create A New User
Node Status?
node history
Node Type: perlquestion [id://428760]
Approved by Paladin
help
Chatterbox?
and the web crawler heard nothing...

How do I use this? | Other CB clients
Other Users?
Others chilling in the Monastery: (5)
As of 2014-07-23 03:09 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?

    My favorite superfluous repetitious redundant duplicative phrase is:









    Results (131 votes), past polls