views:

165

answers:

3

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?

+14  A: 

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.

Dave Webb
He's also using `while`.
Paul Nathan
@Paul - Yes, and `print` and `my`, but the problem is the `for` / `shift` combo.
Chris Lutz
+15  A: 

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 with splice. 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.

Chris Lutz
Was just about to link to the documentation in my own answer. +1
Dave Webb
It's not actually getting that confused (see my tests below). It *is* confusing syntax though!
Axeman
+1  A: 

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 );
Axeman
This is /exactly/ the answer I was looking for. I decided to not push further and just accept the "don't do this" answers because there seemed to be some vague hostility going on, but what I really wanted to know is WHY you shouldn't do this. Thank you!
John