「foreach の $_ は local」、「while の $_ は global」な件について

追記(2007年12月28日(金))

以下の内容には恐らく筆者の勘違いが含まれていると思うので、あんまり鵜呑みにしないでください。あと、ネタ元の id:fbisさんからコメントを頂いたのでそちらも参照してください。整理がついたら内容を随時訂正していく予定です(整理つかないかもしれないけど…)。

追記(2007年12月30日(日))

id:kitsさんがブクマで教えてくれたリンク先「perlsyn - perldoc.perl.org」の foreach の説明がわかり易いかも。要は、LIST 中の要素で一つでも左辺値でなければ、制御変数「$_」の変更は失敗するということらしい。あと、自分が激しく勘違いしてたとこだけど、制御変数「$_」は常に LIST 中の各要素のエイリアスになるみたいです。


whileでファイルハンドルをループする時の暗黙の$_について - Unknown::Programmingを見て理解できなかったので調べてみた。


まずは foreach の動作から確認

ラクダ本第3版 p.137-p.138 から引用
foreach VAR (LIST) {
...
}

LISTのすべての要素が代入可能な値(定数ではなく変数)である場合、ループ内でVARの値を変えれば、(LIST中の)対応する変数の値が変更される。その理由は、foreachループの制御変数は、繰り返しの対象となるリストの各要素へのエイリアスにされるからである。


以上のラクダ本の引用を実際に以下のコードで検証してみる。


これはもちろん正しく動作する。

@numbers は代入可能なリストなので自動的にエイリアス化される
my @numbers = (1..5);
foreach my $num (@numbers) {
  $num += 1;
}
print map "$_\n", @numbers;


ややこしいけど、これも正しい。なぜなら「LISTが代入可能な値じゃない」ので、リストの各要素はエイリアス化されないから。(追記:恐らく(1..5)は代入可能で、エイリアス化はされていると思う。自信なし。詳しくはコメント参照。)

(1..5) は代入不可能(定数)なリストなのでエイリアス化さない
foreach my $num (1..5) {
  $num += 1;
}


しかし、さらにややこしいことに、なぜか「LISTが代入可能な値じゃない」のにも関わらず、文字列のリストはエイリアス化されてしまうのでこれは正しくない。(追記:恐らくqw/ hoge fuga /は代入不可能で、エイリアス化はされていると思う。自信なし。詳しくはコメント参照。)

qw/ hoge fuga / は代入不可能(定数)なリストであるが、エイリアス化される
foreach my $tmp (qw/ hoge fuga /) {
  # "hoge" = "hoge" . "piyo" と等価になってしまう!
  $tmp .= "piyo";
}

# 実行結果
% perl -w hoge.pl
Modification of a read-only value attempted at hoge.pl line 48.


最後に、問題のコードに行く前にもう一つラクダからの引用をしなくちゃならない。

ラクダ本第3版 p.138 から引用

(foreach文において)レキシカル宣言がスコープにない場合には、ループ変数は局所化された(ダイナミックスコープを持つ)グローバル変数になる。この場合、ループの中から呼び出された関数は、ループ変数にアクセスすることができる。


以上の事実をまとめると、問題のコードのどこが悪かったかが分かる。

sub print_file {
  my $file = shift;
  open my $FH , $file or die "$!";
  # これはまずい!
  # このwhileループからは(もちろん関数全体からも)ループ変数の(つまり foreachの中の)$_ が見えるので、
  # 「$_ = <$FH>」は「"hoge.txt" = <$FH>」と等価になってしまう。
  while ( <$FH> ) {
    print $_;
  }
  # 以下はエラーにならない
#   while ( my $hoge = <$FH> ) {
#     print $hoge;
#   }
  close $FH;
}
foreach ( qw/ hoge.txt muge.txt / ) {
  # qw/ hoge.txt muge.txt / は内部的にエイリアス化されて順番に $_ に代入される
  print_file($_);
}


この問題の一番手っ取り早い解決方法は、ややこしい「暗黙の $_」変数を使わないで、最初からレキシカルスコープ変数を使うことだと思う。

sub print_file {
  my $file = shift;
  open my $FH , $file or die "$!";
  while ( <$FH> ) {
    print $_;
  }
  close $FH;
}
foreach my $file (qw/ hoge.txt muge.txt / ) {
  print_file($file);
}

最後に

全体的に断定口調で書いて自信ありげだけど、正直、Perl 初心者な自分には正しいかどうか分からない。激しく勘違いしている可能性もあるので、もし、何か少しでも気づくことがあったらコメントください。お願いします。