DEV Community

Simon Green
Simon Green

Posted on

The dangers of each in Perl

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

Top comments (1)

Collapse
 
jwrightecs profile image
jwrightecs

A few more solutions spring to mind,

OUTER: for (1 .. 5) {
   # use values to get just the colors
    foreach my $color (values %favorite) {
        next OUTER if $color eq 'brown';
    }
    say "Oh dear!";
    exit;
}
Enter fullscreen mode Exit fullscreen mode
OUTER: for (1 .. 5) {
    while (my($name, $color) = each %favorite) {
        next OUTER if $color eq 'brown';
    }
    say "Oh dear!";
    exit;
} continue {
    # reset %favorite iterator on OUTER 
    keys %favorite;
}
Enter fullscreen mode Exit fullscreen mode
OUTER: for (1 .. 5) {
   # use bare each (5.18+)
    while (each %favorite) {
        next OUTER if $favorite{$_} eq 'brown';
    }
    say "Oh dear!";
    exit;
} continue {
    # reset %favorite iterator on OUTER
    keys %favorite;
}
Enter fullscreen mode Exit fullscreen mode