Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Break out of innermost loop in nested while loops in perl: Label not found

Tags:

perl

I have a problem with breaking out of the innermost enclosing loop in doubly nested while loops. Consider:

use v5.14;

my $i=0;
while ($i<=1) {
    my $j=0;
    do {
        last if $j==2;
        say $j++;
    } while ($j<4);
    $i++;
}

The output is here:

0
1

whereas the expected output should be:

0
1
0
1

Adding a label to the innermost enclosing loop:

my $i=0;
while ($i<=1) {
    my $j=0;
    LINE: do {
        last LINE if $j==2;
        say $j++;
    } while ($j<4);
    $i++;
}

gives error:

Label not found for "last LINE" at ./p.pl line 9.
like image 724
Håkon Hægland Avatar asked Apr 20 '14 12:04

Håkon Hægland


5 Answers

You can only use last, next and redo in loops that use a BLOCK.

  • while (EXPR) BLOCK
  • until (EXPR) BLOCK
  • for (LIST) BLOCK
  • for (EXPR; EXPR; EXPR) BLOCK
  • BLOCK

You can't use them on loops created by statement modifiers[1].

  • EXPR while EXPR;
  • EXPR until EXPR;

do BLOCK while EXPR; is just a case of EXPR while EXPR;. The loop doesn't have a BLOCK for last to work on. It's merely a while statement modifier that's modifying do function[2]. The do function being modified has a BLOCK, but do is not a loop.


You could add a loop to last out of, such as a bare loop (BLOCK) that's normally executed only once.

my $i=0;
while ($i<=1) {
    LOOP: {
        my $j=0;
        do {
            last if $j==2;
            say $j++;
        } while ($j<4);
    }
    ++$i;
}

But if we could make sure ++$i always got executed, we could just next the outer loop.

my $i=0;
while ($i<=1) {
    my $j=0;
    do {
        next if $j==2;
        say $j++;
    } while ($j<4);
} continue {
    ++$i;
}

The above simplifies to

for (my $i=0; $i<=1; ++$i) {
    my $j=0;
    do {
        next if $j==2;
        say $j++;
    } while ($j<4);
}

which simplifies to

for my $i (0..1) {
    my $j=0;
    do {
        next if $j==2;
        say $j++;
    } while ($j<4);
}

Of course, that assumes we need to preserve the do while. If not, we can just write

for my $i (0..1) {
    for my $j (0..3) {
        last if $j==2;
        say $j;
    }
}

  1. Or so I thought. Turns out it works for for!!!!

    >perl -we"print($_), (($_ == 5) && last) for 1..10; print(qq{\ndone\n});"
    12345
    done
    
  2. Well, not quite "merely". It's special-cased to be bottom-tested instead of top-tested.

like image 155
ikegami Avatar answered Nov 02 '22 19:11

ikegami


As perldoc do and everyone has already stated:

  • do BLOCK

    Not really a function. Returns the value of the last command in the sequence of commands indicated by BLOCK. When modified by the while or until loop modifier, executes the BLOCK once before testing the loop condition. (On other statements the loop modifiers test the conditional first.)

    do BLOCK does not count as a loop, so the loop control statements next, last, or redo cannot be used to leave or restart the block. See perlsyn for alternative strategies.

The deeper tribal knowledge with regard to this is that you should reserve these statement modifiers for single line or at most two line statements.

If you want a multiline block to be a loop, the coder should be able to observe that from the very first line. Therefore, instead of ever using a statement modifier with a do BLOCK, setup an infinite loop and create your test to break out of it at the end:

while (1) {
    ...

    last if EXPR;
}

This would change your somewhat contrived example to the following:

use v5.14;

my $i = 0;
while ($i <= 1) {
    my $j = 0;
    while (1) {
        last if $j == 2;
        say $j++;
        last if $j >= 4;
    }
    $i++;
}
like image 24
Miller Avatar answered Nov 02 '22 18:11

Miller


You can't use last inside do{} block, but you can

my $i=0;
while ($i<=1) {
    my $j=0;
    LINE: { 
      do {
        last LINE if $j==2;
        say $j++;
      } while ($j<4); 
    }
    $i++;
}

or

my $i=0;
OUTER: while ($i<=1) {
    my $j=0;
    LINE: do {
        next OUTER if $j==2;
        say $j++;
    } while ($j<4);
}
continue {
    $i++;
}
like image 28
mpapec Avatar answered Nov 02 '22 19:11

mpapec


If you really must:

my $i=0;
while ($i++<=1) {
    my $j=0;
    LINE: {
      do {
        last LINE if $j==2;
        say $j++;
      } while ($j<4);
    }
}
like image 23
Zaid Avatar answered Nov 02 '22 20:11

Zaid


As noted by others, you can't use last with do:

do BLOCK does not count as a loop, so the loop control statements next, last, or redo cannot be used to leave or restart the block.

Another solution is to use another while loop:

my $i = 0;
while ($i <= 1) {
    my $j = 0;
    while($j < 4) {
        last if $j == 2;
        say $j++;
    }
    $i++;
}

or, with for loops:

for my $i (0 .. 1) {
    for my $j (0 .. 3) {
        last if $j == 2;
        say $j;
    }
}
like image 45
RobEarl Avatar answered Nov 02 '22 18:11

RobEarl