Reputation: 441
Have a perl brain-teaser:
my @l = ('a', 'b', 'c');
for (@l) {
my $n = 1;
print shift @l while (@l and $n --> 0);
print "\n";
}
What's it print? Should be a, b, and c, right? But oh wait actually there's a bug somewhere, it only prints a and b. Probably just some stupid off-by-one, should be easy to solve, right?
Ok so make a small code change to test things out and change @l to
my @l = ('a', 'b', 'c', 'd');
What's it print? Probably a, b, and c because of that stupid off by one, right? ...Wait a second, actually it still prints only a and b. Okay, so the bug is that it only prints the first two characters.
Change @l again to
my @l = ('a', 'b', 'c', 'd', 'e');
Uhm, now it prints a, b, and c. But not d or e. In fact, every 2 letters we add from now on will make it print the next letter in the sequence. So if we add f it'll still just print a, b, and c, but if we add f and g it'll print a, b, c, and d.
This also happens with similar results for different values of $n.
So what's going on here?
Upvotes: 3
Views: 229
Reputation: 29854
I think this is somebody's gadget code. It doesn't look like the way you would want to write anything. But what it might illustrate best is that (in at least some versions) Perl is really running a more basic for loop, where:
for ( @l ) {
#...
}
Is replaced by:
for ( my $i = 0; $i < @l; $i++ ) {
local $_ = $l[$i];
#...
}
Thus, because @l
is ( 'c' )
when we've gone through twice, our trips through is already greater than scalar( @l )
, so we're out. I've tested it out in a number of cases, and they seem to be equivalent.
Below is the code I wrote to test cases. From it we can see that because of the shift, as soon as we're halfway through, the loop will exit.
use strict;
use warnings;
use English qw<$LIST_SEPARATOR>;
use Test::More 'no_plan';
sub test_loops_without_shifts {
my @l = @_;
my @tests;
for ( @l ) {
push @tests, $_;
}
my @l2 = @_;
my $n = @tests;
my $i = 0;
for ( $i = 0; $i < @l2; $i++ ) {
local $_ = $l2[$i];
my $x = shift @tests;
my $g = $_;
is( $g, $x, "expected: $x, got: $g" );
}
is( $n, $i );
is_deeply( \@l, \@l2, do { local $LIST_SEPARATOR = .', '; "leftover: ( @l ) = ( @l2 )" } );
return $i;
}
sub test_loops {
my @l = @_;
my @tests;
for ( @l ) {
push @tests, shift @l;
}
my @l2 = @_;
my $n = @tests;
my $i = 0;
for ( $i = 0; $i < @l2; $i++ ) {
local $_ = $l2[$i];
my $x = shift @tests;
my $g = shift @l2;
is( $g, $x, "expected: $x, got: $g" );
}
is( $n, $i );
is_deeply( \@l, \@l2, do { local $LIST_SEPARATOR = ', 'c; "leftover: ( @l ) = ( @l2 )" } );
return $i;
}
is( test_loops( 'a'..'c' ), 2 );
is( test_loops( 'a'..'d' ), 2 );
is( test_loops( 'a'..'e' ), 3 );
is( test_loops( 'a'..'f' ), 3 );
is( test_loops( 'a'..'g' ), 4 );
is( test_loops_without_shifts( 'a'..'c' ), 3 );
is( test_loops_without_shifts( 'a'..'d' ), 4 );
is( test_loops_without_shifts( 'a'..'e' ), 5 );
is( test_loops_without_shifts( 'a'..'f' ), 6 );
is( test_loops_without_shifts( 'a'..'g' ), 7 );
Upvotes: 1
Reputation: 75399
Dave Webb beat me to the problem, but here's a quote from perldoc perlsyn
saying not to do it:
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 withsplice
. So don't do that.
Note that, earlier in the text, the syntax of foreach
was described as foreach LIST
, which is the LIST
they refer to in the documentation. Note also that foreach
and for
are equivalent.
Upvotes: 15
Reputation: 193716
What's going on is that you're using for
and shift
at the same time. So you're looping through the list whilst modifying it, not a good idea.
Upvotes: 14