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

I present the following code:
my $match = '1995_Olga_goes_to_Egypt'; my @pictures = qw{1995/1995_Olga_first_time_in_Cyprus/06330004.JPG 1995/1995_Olga_goes_to_Egypt/QD0017004.JPG}; foreach $picture (sort @pictures) { print "Debug: $picture\n"; if ($picture =~ m/$match/) { print "Matched, $picture\n"; last; } } print "Result: $picture\n";
Which produces:
buu@ded430-deb-175-30:~/test$ perl test.pl Debug: 1995/1995_Olga_first_time_in_Cyprus/06330004.JPG Debug: 1995/1995_Olga_goes_to_Egypt/QD0017004.JPG Matched, 1995/1995_Olga_goes_to_Egypt/QD0017004.JPG Result:
Why isn't $picture set after the loop terminates?

Replies are listed 'Best First'.
Re: Last undefines a for loop's itererator?
by dave_the_m (Monsignor) on Nov 12, 2005 at 10:53 UTC
    Why isn't $picture set after the loop terminates?
    Because at loop exit, $picture is restored to whatever value it had before loop entry (in this case undef); or more precisely: within the loop, $picture is aliased to each value in turn; outsise the loop it is not aliased and so the name $picture once again refers to the original value.

    Dave.

      Some additional comment on this: you really don't want $picture to be set outside the loop. It would make debbuging really hard.

      As [id://dave_the_m] said: it is an alias. Meaning if you do $picture = "something else"; then the original value will change. Limiting the effect of the alias to the foreach-block (and everything that is called via the foreach-block and still is in the lexical-scope of the variable) is a really good thing. (Because it reduces the code you need to look at when debugging.)

      Also note that it is not last that undefines it. For example: for my $x (0 .. 3) { } print $x;==> it won't print 3.

      (Ofcourse in your example you won't see the difference after doing an assignment because you are using a temporary list.)(It does matter. Sort seems to return an alias.)

      Update: added comment about last
      Update2: stroke some incorrect text.

      Just add one thing, with use strict and warnings, Perl does give error: (I added my in front of $picture, so that the error at line 15 is isolated)

      use strict; use warnings; my $match = '1995_Olga_goes_to_Egypt'; my @pictures = qw{1995/1995_Olga_first_time_in_Cyprus/06330004.JPG 1995/1995_Olga_goes_to_Egypt/QD0017004.JPG}; foreach my $picture (sort @pictures) { print "Debug: $picture\n"; if ($picture =~ m/$match/) { print "Matched, $picture\n"; last; } } print "Result: $picture\n";

      This gives:

      Global symbol "$picture" requires explicit package name at math1.pl li +ne 15. Execution of math1.pl aborted due to compilation errors.
        That's not relevant to the issue here. The following strict-compliant program illustrates the issue:
        use strict; use warnings; my $x; for $x (1 .. 3) { last if $x == 2 } print $x;
        which prints
        Use of uninitialized value in print at - line 5.
Re: Last undefines a for loop's itererator?
by sauoq (Abbot) on Nov 12, 2005 at 15:56 UTC

    The relevant documentation from perlsyn:

    The "foreach" loop iterates over a normal list value and sets the variable VAR to be each element of the list in turn. If the variable is preceded with the keyword "my", then it is lexically scoped, and is therefore visible only within the loop. Otherwise, the variable is implicitly local to the loop and regains its former value upon exit- ing the loop. If the variable was previously declared with "my", it uses that variable instead of the global one, but it's still localized to the loop.

    -sauoq
    "My two cents aren't worth a dime.";
    
Re: Last undefines a for loop's itererator?
by jonix (Friar) on Nov 12, 2005 at 11:19 UTC
    This might do what you want:
    #!/usr/bin/perl use strict; use warnings; my $match = '1995_Olga_goes_to_Egypt'; my @pictures = qw{1996/1996_Olga_goes_to_Egypt/QD0017005.JPG 1995/1995 +_Olga_first_time_in_Cyprus/06330004.JPG 1995/1995_Olga_goes_to_Egypt/QD0017004.JPG}; my $picture; foreach (sort @pictures) { print "Debug: $_\n"; if (m/$match/) { print "Matched, $_\n"; $picture = $_; last; } } print "Result: $picture\n";
    This assigns only the first matched element of @pictures to the lexical my $picture before dropping out of the loop.
Re: Last undefines a for loop's itererator?
by ivancho (Hermit) on Nov 12, 2005 at 17:24 UTC
    after all, you wouldn't want every loop
    foreach (0..10) { whatever }
    or even
    print for @bla;
    to clobber up your $_, no? So it sounds logical that the control variable should be localized..

    kaif, why don't you use first, from List::Util, if the snippet is small enough?
      Hmm. I missed this reply to my post for a while since it's not quite a reply to my post. :-)

      I guess I should use first --- I may have to experiment to see if it handles code with side-effects like I would desire. Thanks a bunch.

      (And, yeah, $_-clobbering is, in my experience, something terrible that eventually every beginning Perl programmer runs into and spends hours debugging. Phew!)

      OK, I guess your remark confuses me a bit:
      What do you mean with for/foreach clobbering up $_?
      Is $_ not always implicitly localized in these looping constructs as a lexical (while while is not really a looping construct) - what is the problem with using $_?

      Cheers,
      jonix
        $_ is, indeed, localized in for(|each) loops. I was using this to support the claim that any other (explicit) loop variable would be better off localized too. Using $_ is, of course, perfectly fine.
        I quite like ewilhelm's point - if one wants a loop that behaves like a function, why not write one.

        I guess there's some sort of linguistic argument here as well - if we say "This cow is green. Each cow in the field is purple. The cow has red eyes", in the last sentence 'cow' has clearly reverted to mean the first cow again, even though it's been pointing to other cows in the 'Each' sentence..

Re: Last undefines a for loop's itererator?
by Aristotle (Chancellor) on Nov 12, 2005 at 21:47 UTC

    Indeed, as has been mentioned:

    use List::Util qw( first ); my $picture = first { /$match/ } sort @pictures;

    Without List::Util, I’d use a while:

    my $picture = do { my @candidate = sort @pictures; shift @candidate while @candidate and $candidate[0] !~ /$match/; shift @candidate; };

    After all, you need a temporary array to store the sorted list anyway – so you can afford to iterate it destructively, and thus you get to use while( @list ), which lets you get away without much synthetic index fiddling.

    Update: fixed wrong sigil on 2nd shift; thanks, Tanktalus.

    Makeshifts last the longest.

Re: Last undefines a for loop's itererator?
by kaif (Friar) on Nov 12, 2005 at 16:11 UTC
    So everyone has explained why this behavior happens, and I've been bitten by this myself. Every time that I code
    for $i ( 1 .. $foo ) { last if bar($i); } print $i;
    or something like it, I end up having to change that to
    for( $i = 1; $i <= $foo; $i++ ) { last if bar($i); } print $i;
    I want to still use foreach, but I never can, so I end up using C-style for more and more these days. Is there any good idiom that uses foreach, but yet still lets me access the variables afterwards? (And no, the chunk is far too small for a subroutine.)
      for $i ( 1 .. $foo ) { last if bar($i); } print $i;
      Is there any good idiom that uses foreach, but yet still lets me access the variables afterwards? (And no, the chunk is far too small for a subroutine.)
      use warnings; use strict; my $i = sub { for my $i (1..5) { return($i) if ($i == 4); } }->(); print $i;

      If it doesn't work under warnings and strict, there is probably a good reason for that. IMO, if you want to access the loop variable outside the loop, you should declare a separate variable and set it explicitly before you drop out of the loop.

      If what you really want is a loop that behaves like a subroutine, why not just write it as such?

        If it doesn't work under warnings and strict, there is probably a good reason for that. IMO, if you want to access the loop variable outside the loop, you should declare a separate variable and set it explicitly before you drop out of the loop.
        It does work fine under warnings and strict, if you don't declare your variable in the conditions on the for(..) loop. It has always seemed weird to me to write code like
        my $iold; for my $i ( 1 .. $foo ) { if( bar($i) ) { $iold = $i; last; } }
        Doesn't it just seem like a waste of variables? You're clearly using $i and $iold for the same purpose.

        As for why it isn't a subroutine, perhaps because I like this code all in one place. Shrug.

      I want to still use foreach

      Why? A C-style for is perfectly acceptable here. That is, so long as you use it correctly. For the example you gave to match your foreach loop, you'd have to change your condition to $i < $foo because your current code, $i <= $foo, will leave you with $i being one more than $foo if you get to the end of your loop.

      -sauoq
      "My two cents aren't worth a dime.";
      
        I disagree that my loops aren't equivalent. It is common practice in C to write loops of the form
        for( $i = 0; $i <= $foo; $i++ ) { last if bar( $foo ); } if( $i > $foo ) { # Condition never met! }
        Essentially, what I would like to do is distinguish between bar(..) being true for the last element in my last, and never being true at all. In "my perfect little world", I would like foreach to unset its iterator variable if and only if it loops through the entire list without encountering last.

        So, in my example above, if bar( $foo ) is the first to be true, I want $i == $foo; if bar(..) was never true, it'd be nice to have $i be undef.

        In addition, if bar(..) has side effects, then our code is not equivalent.

        Edit: Also, I prefer a foreach-style loop because all I'm doing is iterating -- precisely the designated purpose of foreach. Otherwise, if I have a long variable name, I would have to type for( $long_variable_name = 1; $long_variable_name <= 5; $long_variable_name ++ ) versus for $long_variable_name ( 1 .. 5 ). I'm pretty sure I'm not the only one who has made copy-paste errors like for( $j = 0; $j <= 5; $i++ ).

      Rather than using $i as your iterator, use the default $_. Then assign it's value to your 'custom iterator' inside the foreach.

      foreach ( 1 .. $foo ) { $i = $_; last if bar($i); } print $i;
        Thank you very much. Of all the solutions, I like this the best.

      The problem with iterating over indices is that BUU is iterating over a sorted temporary list, so if you don’t create it en passant in the foreach() list, you need to store it in a temporary array variable. And since you have a temp array, you can afford to destroy it, so the simplest approach is while( @list ) { ...; shift @list }.

      Makeshifts last the longest.

A reply falls below the community's threshold of quality. You may see it by logging in.