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

Hi Monks,

After spending an hour or two tracking down a bug, I re-discovered the following information, which is clearly stated in the each() docs:

There is a single iterator for each hash, shared by all each(), keys(), and values() function calls in the program; it can be reset by reading all the elements from the hash, or by evaluating keys HASH or values HASH.

OK, getting to the point, my program has a global HASH, and several places where I iterate over the elements using each(). In one of those places, the iteration is inside another loop. Now, the interesting part:

Sometimes I need to restart the outer loop from inside the each() loop.

The following code illustrates:

my %hash = ( 'foo' => 1, 'bar' => 2, 'baz' => 3, ); OUTER_LOOP: foreach my $item qw(this that the_other) { print "$item:\n"; # keys %hash; # this resets the iterator while (my ($key, $value) = each %hash) { print " $key => $value\n"; if ($item eq 'that' && $key eq 'baz') { print "bailing out!\n"; next OUTER_LOOP; } } }

When I run this, I get:

this: foo => 1 baz => 3 bar => 2 that: foo => 1 baz => 3 bailing out! the_other: bar => 2

As you can see, when I bail out, the iteration has not completed, and the next call to each() gets me the next element in the HASH, rather than starting over again, which is What I Want.

The keys() docs states:

As a side effect, it resets HASH's iterator.

So.... if you uncomment that # keys %hash; line in my program, then you get the following:

this: foo => 1 baz => 3 bar => 2 that: foo => 1 baz => 3 bailing out! the_other: foo => 1 baz => 3 bar => 2

...which is More Like It.

So... my question (yes, there is a question buried down here...) is this:

Is this an idiom that I just haven't seen, whereby you put a call to keys() before your calls to each(), when you have this kind of situation? The wording of the keys() doc describing this behavior as a 'side effect' doesn't warm my heart, and leads me to believe it might be leading my down the dark road of Obfuscated Code... Is there another, well-accepted way to accomplish this? Also bear in mind that my real code doesn't resemble this contrived example, and I have calls to each() in other functions which will need the call to keys() in order to work correctly...

What say my fellow monks?

--
3dan

Replies are listed 'Best First'.
Re: hash iteration with each
by diotalevi (Canon) on Mar 13, 2003 at 14:18 UTC

    This is the stuff of which objects are made. Seriously. You'll want to secret your hash data away somewhere so another programmer isn't going to stomp all over your iterator by accident. The alternative views would require you to properly scope your global hash (so its not global anymore) or commit to iterating over the keys() result.

    Either that or just be very, very careful. (and use scalar values() over keys() to reset because its supposed to be faster (since I can't be bothered to benchmark that for you)).


    Seeking Green geeks in Minnesota

Re: hash iteration with each
by tommyw (Hermit) on Mar 13, 2003 at 15:25 UTC

    if you replace

    while (my ($key, $value) = each %hash) {
    with
    foreach my $key (keys %hash) { my $value=$hash{$key};
    , it will behave as you want, without risk of nasty side-effects.

    (Which begs an efficiency question. Unfortunately, I'm not qualified to answer that one.)

    --
    Tommy
    Too stupid to live.
    Too stubborn to die.

(jeffa) Re: hash iteration with each
by jeffa (Bishop) on Mar 13, 2003 at 15:02 UTC
    I like diotalevi's answer better than this ... but, TIMTOWTDI!
    my %hash = ( foo => 1, bar => 2, baz => 3, ); OUTER: for (map{[$_,{%hash}]} qw(this that the_other)) { my ($item,$hash) = @$_; print "$item:\n"; while (my ($key, $value) = each %$hash) { print " $key => $value\n"; if ($item eq 'that' && $key eq 'baz') { print "bailing out!\n"; next OUTER; } } }
    The idea is give eash item in the array it's own copy of the hash. This obviously is going to be memory-intensive when scaled up.

    jeffa

    L-LL-L--L-LL-L--L-LL-L--
    -R--R-RR-R--R-RR-R--R-RR
    B--B--B--B--B--B--B--B--
    H---H---H---H---H---H---
    (the triplet paradiddle with high-hat)
    
Re: hash iteration with each
by jonadab (Parson) on Mar 14, 2003 at 02:56 UTC
    The wording of the keys() doc describing this behavior as a 'side effect' doesn't warm my heart, and leads me to believe it might be leading my down the dark road of Obfuscated Code...

    Lucid comments are very helpful. Something like this can hardly be considered obfuscated:

    OUTER_LOOP: foreach my $item qw(this that the_other) { print "$item:\n"; keys %hash; # keys resets the iterator on %hash so that our # subsequent calls to each will get all the # pairs in the hash. This is necessary in # case the iterator was left in mid-hash # by a previous loop-through that was aborted. while (my ($key, $value) = each %hash) { print " $key => $value\n"; if ($item eq 'that' && $key eq 'baz') { print "bailing out!\n"; next OUTER_LOOP; } } }

    for(unpack("C*",'GGGG?GGGG?O__\?WccW?{GCw?Wcc{?Wcc~?Wcc{?~cc' .'W?')){$j=$_-63;++$a;for$p(0..7){$h[$p][$a]=$j%2;$j/=2}}for$ p(0..7){for$a(1..45){$_=($h[$p-1][$a])?'#':' ';print}print$/}
      Far too much comment, if you ask me. I'd rather do something like
      # just in case we aborted a previous iteration half-way through # (see perldoc -f each) keys %hash;
      No reason to bloat the code with things better explained by the documentation.

      Makeshifts last the longest.