smackdab has asked for the wisdom of the Perl Monks concerning the following question:

Hi, this works and I would like to know how Perl keeps track of its place in the shrinking I can sleep better ;-)

use strict; my @list = (1,2,3,4,5); foreach (@list) { if ($_ == 2 || $_ == 4) { unshift (@list); next; } print "$_\n"; }

Replies are listed 'Best First'.
Re: walking an array and unshifting
by graff (Chancellor) on Jun 09, 2002 at 03:42 UTC
    The reason that your script works is that it isn't really changing the array. You're appending an empty set to it.

    If there were one or more values after the array name in the unshift call, the script would print "1", then consume all available memory before doing anything else.

    update: I should have said "you're not prepending anything to the array.

    Also, the script might print more things before running out of memory, if you happen to prepend more than one element at a time, and the second element of that set was not "2" or "4". But the script will still blow up.

      After reading your post, I am concerned ;-)

      I thought that each "unshift" was removing the first entry in the array(ie $array|0|)...where does the "empty set" come in???

        perldoc -f unshift unshift ARRAY,LIST Does the opposite of a "shift". Or the opposite of a "push", depending on how you look at it. Prepends list to the front of the array, and returns the new number of elements in the array. unshift(ARGV, '-e') unless $ARGV[0] =~ /^-/; Note the LIST is prepended whole, not one element at a time, so the prepended elements stay in the same order. Use "reverse" to do the reverse.
        addendum: If you choose to use shift instead of unshift, you will not run out of memory, and you will see only one element ("1") printed out, which is maybe what you were expecting in the first place.
Re: walking an array and unshifting
by tadman (Prior) on Jun 09, 2002 at 05:56 UTC
    From what I can tell, if you pull the carpet out from underneath Perl, you're going to pay the price. One way to get things on the same wavelength is to either use grep to do your list filtering:
    @list = grep { $_ != 2 && $_ != 4 } @list;
    Or, you can go all Old School and do it this way:
    my @foo = 1..10; for (my $i = 0; $i < @foo; $i++) { print "$i ($foo[$i])\n"; if ($foo[$i] == 5 || $foo[$i] == 6) { splice(@foo, $i, 1); redo; } }
    The redo is important because it prevents the for loop from incrementing $i and thereby skipping an entry. This way, you're keeping pretty "close to the metal" and nothing will slip by.

    As merlyn has suggested, this so-called "Old School" code is not an example of how it should be done, and is in fact, an example to the contrary. Using map and grep is going to be more effective and less prone to programmer error virtually every time, so that's what I do. With the example, I was merely trying to show how difficult it was to do if you chose not to use grep.
      That's far too complicated a solution for me to leave standing after a code review if I was doing this formally.

      If you merely want to avoid processing items based on their value, use a next in a foreach:

      for $item (@list) { next if $item == 2 or $item == 4; ... rest of processing .. }
      If you want a list that contains all but those items, then use a grep, as you said:
      my @newlist = grep { $_ != 2 and $_ != 4 } @list;
      But your for-loop monstrosity is ripe for off-by-one errors, and even if you got everything just right, your maintenance programmer would almost certainly break it.

      Extra special hint: walking a list with for-style loops is almost always WRONG. Yes, there are counterexamples, but start with that.

      -- Randal L. Schwartz, Perl hacker

A reply falls below the community's threshold of quality. You may see it by logging in.