The dangers of each in Perl

Simon Green - May 14 '23 - - Dev Community

It's been a very long time that I've made a post that wasn't related to The Weekly Challenge, but it's about time I did :)

Today's post is about the unexpected output of a script when using the each function in Perl inside a loop.

Example script

Let's take a look at an oversimplified example:

#!/usr/bin/env perl

use strict;
use warnings;
use feature 'say';

my %favorite = (
    Simon => 'blue',
    Tom   => 'brown',
    Dick  => 'brown',
    Harry => 'red',
);

OUTER: for (1 .. 5) {
    while (my($name, $color) = each %favorite) {
        next OUTER if $color eq 'brown';
    }

    say "Oh dear!";
    exit;
}
Enter fullscreen mode Exit fullscreen mode

This seems straight forward enough. We have an outer loop that iterates five times. If any person has a favorite color of 'brown' we exit the inner loop, and 'Oh dear!' is never printed. So lets run that.

$ ./each.pl 
Oh dear!
Enter fullscreen mode Exit fullscreen mode

What went wrong

Even to an experienced Perl developer, this is probably not the expected result. So what happened?

Lets add some debugging output to the script

OUTER: for (1 .. 5) {
    say "count: $_";
    while (my($name, $color) = each %favorite) {
        say "name: $name, color: $color";
        next OUTER if $color eq 'brown';
    }

    say "Oh dear!";
    exit;
}
Enter fullscreen mode Exit fullscreen mode

The output is

$ ./each.pl 
count: 1
name: Harry, color: red
name: Tom, color: brown
count: 2
name: Dick, color: brown
count: 3
name: Simon, color: blue
Oh dear!
Enter fullscreen mode Exit fullscreen mode

What this shows is that even though we restart the outer loop, the inner each loop does not reset for each iteration.

This is documented in the each man page.

The iterator used by each is attached to the hash or array, and is shared between all iteration operations applied to the same hash or array. Thus all uses of each on a single hash or array advance the same iterator location.

Solution

The easiest solution is to always use the keys function when iterating over a hash inside a loop. Therefore a possible solution would be to write something like the below.

OUTER: for (1 .. 5) {
    foreach my $name (keys %favorite) {
        next OUTER if $favorite{$name} eq 'brown';
    }

    say "Oh dear!";
    exit;
}
Enter fullscreen mode Exit fullscreen mode

This produces no output, as one would expect.

$ ./each.pl 
$
Enter fullscreen mode Exit fullscreen mode
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .