Automatically get loop index in foreach loop in Perl

Viewed 124338

If I have the following array in Perl:

@x = qw(a b c);

and I iterate over it with foreach, then $_ will refer to the current element in the array:

foreach (@x) {
    print;
}

will print:

abc

Is there a similar way to get the index of the current element, without manually updating a counter? Something such as:

foreach (@x) {
    print $index;
}

where $index is updated like $_ to yield the output:

012
13 Answers

Like codehead said, you'd have to iterate over the array indices instead of its elements. I prefer this variant over the C-style for loop:

for my $i (0 .. $#x) {
    print "$i: $x[$i]\n";
}

In Perl prior to 5.10, you can say

#!/usr/bin/perl

use strict;
use warnings;

my @a = qw/a b c d e/;

my $index;
for my $elem (@a) {
    print "At index ", $index++, ", I saw $elem\n";
}

#or

for my $index (0 .. $#a) {
    print "At index $index I saw $a[$index]\n";
}

In Perl 5.10, you use state to declare a variable that never gets reinitialized (unlike ones created with my). This lets you keep the $index variable in a smaller scope, but it can lead to bugs (if you enter the loop a second time it will still have the last value):

#!/usr/bin/perl

use 5.010;
use strict;
use warnings;

my @a = qw/a b c d e/;

for my $elem (@a) {
    state $index;
    say "At index ", $index++, ", I saw $elem";
}

In Perl 5.12 you can say

#!/usr/bin/perl

use 5.012; # This enables strict
use warnings;

my @a = qw/a b c d e/;

while (my ($index, $elem) = each @a) {
    say "At index $index I saw $elem";
}

But be warned: you there are restrictions to what you are allowed to do with @a while iterating over it with each.

It won't help you now, but in Perl 6 you will be able to say

#!/usr/bin/perl6

my @a = <a b c d e>;
for @a Z 0 .. Inf -> $elem, $index {
    say "at index $index, I saw $elem"
}

The Z operator zips the two lists together (i.e. it takes one element from the first list, then one element from the second, then one element from the first, and so on). The second list is a lazy list that contains every integer from 0 to infinity (at least theoretically). The -> $elem, $index says that we are taking two values at a time from the result of the zip. The rest should look normal to you (unless you are not familiar with the say function from 5.10 yet).

perldoc perlvar does not seem to suggest any such variable.

Not with foreach.

If you definitely need the element cardinality in the array, use a 'for' iterator:

for ($i=0; $i<@x; ++$i) {
  print "Element at index $i is " , $x[$i] , "\n";
}

No, you must make your own counter. Yet another example:

my $index;
foreach (@x) {
    print $index++;
}

when used for indexing

my $index;
foreach (@x) {
    print $x[$index]+$y[$index];
    $index++;
}

And of course you can use local $index; instead my $index; and so and so.

Oh yes, you can! (sort of, but you shouldn't). each(@array) in a scalar context gives you the current index of the array.

@a = (a..z);
for (@a) { 
  print each(@a) . "\t" . $_ . "\n"; 
}

Here each(@a) is in a scalar context and returns only the index, not the value at that index. Since we're in a for loop, we have the value in $_ already. The same mechanism is often used in a while-each loop. Same problem.

The problem comes if you do for(@a) again. The index isn't back to 0 like you'd expect; it's undef followed by 0,1,2... one count off. The perldoc of each() says to avoid this issue. Use a for loop to track the index.

each

Basically:

for(my $i=0; $i<=$#a; $i++) {
  print "The Element at $i is $a[$i]\n";
}

I'm a fan of the alternate method:

my $index=0;
for (@a) {
  print "The Element at $index is $a[$index]\n";
  $index++;
}

Well, there is this way:

use List::Rubyish;

$list = List::Rubyish->new( [ qw<a b c> ] );
$list->each_index( sub { say "\$_=$_" } );

See List::Rubyish.

Please consider:

print "Element at index $_ is $x[$_]\n" for keys @x;

You shouldn't need to know the index in most circumstances. You can do this:

my @arr = (1, 2, 3);
foreach (@arr) {
    $_++;
}
print join(", ", @arr);

In this case, the output would be 2, 3, 4 as foreach sets an alias to the actual element, not just a copy.

Related