http://qs321.pair.com?node_id=439714

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

I have a general question about foreach loops. Usually I like to loop through things with the perlish:
foreach my $line (@arr) { ## stuff goes here; }

Occasionally I need to access the array element prior to the current one that I am on based on what I see in the current element. When this is the case, I will usually fall back to the c-style:
for (my $x = 0; $x < $#arr; $x++) { my $line = $arr[$x]; ## stuff goes here; }

Which obviously does allow me to get to where I need to be. My question is, is there a way to access the index of the current element in a "perlish" foreach loop, so that I don't need to go the c-style way? I much prefer the cleaner interface on the perl way of doing things.

Thanks!!

Replies are listed 'Best First'.
Re: Foreach Loops
by ikegami (Patriarch) on Mar 15, 2005 at 18:58 UTC

    You can do this, which is somewhat better.

    for my $x (0..$#arr) { my $line = $arr[$x]; ## stuff goes here; }

    It's even optimized to not create the list.

      And, of course, the previous line is available at $arr[$x - 1]

      (unless we're talking $x == 0)

        Well, if you consider arrays to be circular, if $x == 0, then $arr[$x - 1] is the last element. Which would be just right. Unfortunally, it doesn't work the way. $arr[$#arr + 1] isn't the same as $arr[0].
      You say that for my $x (0 .. $#arr) is "better" than for (my $x = 0; $x < @arr; $x ++). Now, I don't care much what you find better, but since you have a preference that you find important enough to advocate, I'm interested in why you find your preference "better".
      It's even optimized to not create the list.
      Yeah, because it's special cased. The C-style loop doesn't create a list at all, and is even "optimized" in older versions of Perl.
        I'm interested in why you find your preference "better".

        The OP asked for something Perl-ish, so anything more Perl-ish than the C-ish code he provided is better in this context. Since C doesn't have the .. operator, this qualifies.

        I also think it's more readable. Instead of having to deal with three expressions, three operators and a keyword inside for's parens, the solution I presented has one operator. The iterator's decleration is even sperated from the bounds for extra readability.

        Yeah, because it's special cased. The C-style loop doesn't create a list at all, and is even "optimized" in older versions of Perl.

        My point was that the performance isn't any worse than the C version, contrary to what I once thought, and contrary to what many probably think.

Re: Foreach Loops
by holli (Abbot) on Mar 15, 2005 at 19:05 UTC
    Well, if you really need just the previous element you can go with
    my $lline; foreach my $line (@arr) { ## stuff goes here; $lline = $line; }


    holli, /regexed monk/
Re: Foreach Loops
by perlfan (Vicar) on Mar 15, 2005 at 19:40 UTC
    You can have a counter:
    my $i = 0; foreach (@myarray) { # stuff goes here $i++; }
    A slight improvement over holli's in that you can access the entire i-1 previous elements.
      You should really put the counter increment in the continue block, though, in case of next or redo. Compare:
      my @arr = ('a'..'m'); my $i=0; foreach (@arr) { # stuff goes here next if $i%4 == 0; print "$i: $_\n"; } continue { ++$i } $i = 0; foreach (@arr) { # stuff goes here next if $i%4 == 0; print "$i: $_\n"; ++$i; }

      Caution: Contents may have been coded under pressure.
Re: Foreach Loops
by Roy Johnson (Monsignor) on Mar 15, 2005 at 20:52 UTC
    my @arr = qw(cat dog pig horse donkey); for my $line (@arr) { my $index = (1=~//..1=~/0/)-1; print "$index = $line\n"; }
    ;-)

    Another Bad Perl Solution.®
      Can you explain what the
      my $index = (1=~//..1=~/0/)-1;
      does??
        I'm so glad you asked!

        I am using a flip-flop as a counter. Of the conditions I gave it, the left side is always true and the right is always false, so the expression is always true. It is a property of the operator that it returns a count of the number of consecutive times it has been true. So subtracting one from that gives you the index.

        Unless there are redos or nexts, which would throw it off. So I really should have written

        my $index = 0; for my $line (@arr) { next if $index % 4 == 0; print "$index = $line\n"; } continue { $index = (1=~//..1=~/0/); }
        Well, actually, I shouldn't have written it at all as a solution to this question. But I like putting little-used operators into the spotlight so that people might think of them when they actually are appropriate.

        Caution: Contents may have been coded under pressure.
Re: Foreach Loops
by Roy Johnson (Monsignor) on Mar 15, 2005 at 22:00 UTC
    Updated: MapCar was a bad choice. This one is actually pretty cool:
    use Algorithm::Loops 'NestedLoops'; my $iter = NestedLoops([\@arr, do {my $i=0; sub {[$i++]}} ]); while (my ($line, $x) = &$iter) { print "$x: $line\n"; }
    Obviously, modifying $line isn't going to affect @arr, though. To do that, you'd have to do
    use Algorithm::Loops 'NestedLoops'; my $iter = NestedLoops([[\(@arr)], do {my $i=0; sub {[$i++]}} ]); while (my ($line, $x) = &$iter) { for my $line ($$line) { print "$x: $line\n"; $line = 'X'; } } print "@arr\n";

    Caution: Contents may have been coded under pressure.
Re: Foreach Loops
by sh1tn (Priest) on Mar 15, 2005 at 19:26 UTC
    Not so clean:
    for(0..@arr){ $arr[$_] }
    Update:
    for(1..@arr){ print $arr[--$_], $_, $/ } # diff ?


      And not so right. You're off by one!

      $ perl -e '@a = (1, 2, 3); for (0..@a) { print "A$_: $a[$_]\n" }' A0: 1 A1: 2 A2: 3 A3:

      -sam

        That's why I said "Not so clear":
        for(1..@arr){ print $arr[--$_], $_, $/ }


Re: Foreach Loops
by hubb0r (Pilgrim) on Mar 15, 2005 at 19:20 UTC
    I guess what I was really looking for was some type of function like:
    get_index_of($line)
    which would provide me just that, the current index. Either way what you've all provided me with is basically the different methods that I've already been using. I basically was just looking for something that fit the generic foreach loop without having to fall back to explicitly indexing the array.

    Thanks for your input
      Such a get_index_of operator would be inefficient, hence it doesn't make much sense for Perl to have it.

      Think about it - an array is a linear structure meant specifically for random-access of index->element. To find the index of some element you will either have to go through the array looking for the element - this is O(N). You can have more time-efficient algorithms at the cost of space, like having a "mirror" hash of element->index.

      As for your original problem, I also sometimes feel a little itch when I have to give up foreach once access to elements other-than-the-current is required.

Re: Foreach Loops
by reasonablekeith (Deacon) on Mar 16, 2005 at 11:10 UTC
    What do you think of the following? It needs a package variable of $main::doforeach_index, but providing you don't override this value you can do nested loops galore with an automagic index. Shame it's the wrong way around. I couldn't swap it round, can anyone else?
    use strict; sub doforeach (&@) { my $coderef = shift; my $index=0; for (@_) { no strict "vars"; $doforeach_index = $index; &$coderef; $index++; } } sub doforeach_index () { return our $doforeach_index; } doforeach { print doforeach_index . "=$_\n" } ('one','two','three'); #prints... #0=one #1=two #2=three
Re: Foreach Loops
by kelan (Deacon) on Mar 17, 2005 at 13:45 UTC

    Other monks have given you some good answers to your question above, but I wanted to point out an error in your code in case it wasn't a typo. In your C-ish style loop:

    for (my $x = 0; $x < $#arr; $x++) {} # ^^^^^^^
    the condition you're using will prevent the loop from running for the final array element, because you only enter the loop when the counter is strictly less than the last index of the array. To correct it, simply change to one of these:
    for (my $x = 0; $x <= $#arr; $x++) {} # ^^ # up to *and including* the last index for (my $x = 0; $x < @arr; $x++) {} # ^ # when used in scalar context, '@arr' returns the number of # elements in @arr

Re: Foreach Loops
by NetWallah (Canon) on Mar 17, 2005 at 00:42 UTC
    I'm surprised no one has suggested using map for this purpose. That way, it is easy to track the previous element ..
    my $prev; map { print qq($prev,$_ \n); $prev=$_} qw(uno dos tres); ##Output ,uno uno,dos dos,tres
    Update: I just noticed that holli's solution above is functionally identical.

        ..."I don't know what the facts are but somebody's certainly going to sit down with him and find out what he knows that they may not know, and make sure he knows what they know that he may not know, and that's a good thing. I think it's a very constructive exchange," --Donald Rumsfeld