each ではまった

Perl には,ハッシュのキーと値を対にしてとることのできる each というオペレータというか関数というかがあるのですが,

my %h = (
  'a' => 1,
  'b' => 2,
  ...
);

foreach my $key (keys %h) {
  print $key, " => ", $h{$key}, "\n";
}

# equivalent to:
while (my ($key, $value) = each %h) {
  print $key, " => ", $value, "\n";
}

この機能は,ハッシュに内在するイテレータを内部的に操作しています。すべてのハッシュの走査が終わるとイテレータはリセットされます(先頭に戻ります)。それでこのイテレータは keys, values などの関数でも使っています。ですから上記の each の下のブロックで keys や values を使うと破綻するそうです(詳しくは perldoc -f each を参照のこと)。

といった話をしばらく前にどなたかのブログfbisさんのところ(でした!ブクマコメントサンクス)の米欄で教えて頂いて(場所を失念してしまいました…)頭の隅に残っていたはずなのですが,やってしまいました。

my %h = ( ... );

sub print_hash {
  while (my ($key, $value) = each %h) {
    if ($key eq 'b') {
      print "${key} => ${value}\n";
      return;
    }
  }
}

この print_hash() を何回も呼び出すと,2回に1回しかうまく動かない状況になってしまいました。これは,return で戻った時にイテレータがリセットされずに,次に呼び出されたときに前回の続きから走査するからなんですね。

このイテレータは上で書いているように keys や values と共用されているので,対処法としては,

sub print_hash {
  while (my ($key, $value) = each %h) {
    if ($key eq 'b') {
      print "${key} => ${value}\n";
      keys %h;  # reset iterator
      return;
    }
  }
}

のように,keys などでイテレータをリセットしてやる…というのは冗談で,複雑なロジックになってきたらすっぱり each を使うのをあきらめたほうがよいと思います。個人的には単純に key と value の組がほしくて全データを走査するとわかっているときに,見た目のロジックがすっきりするなら each を使う,という判断基準です。