defined テストのある while と、ない while

この記事は、2007 年 1 月 27 日に書いた while (defined($line = )) と while ($line = ) の関連記事です。


入力演算子を使って行を読む場合には、こうではなく:

while ($line = <FILE>) {
    # do something
}

こう書かなきゃならないものだと思っていた:

while (defined($line = <FILE>)) {
    # do something
}


なぜかというと、読み込んでくるファイルの一番最後の行に、改行文字で終わっていない "0" という一文字が含まれている可能性があって、それを取りこぼしたくないから。


Perl では、"""0"0 (それぞれ、空文字、文字列のゼロ、数値のゼロ) は、ブール値コンテキストで「偽」とみなされてしまうので、最後の最後に改行文字のついていない "0" (文字列のゼロ)が入力されてくると、while 文から処理が出てしまう。


そう思っていたのに、実際に実験してみると動きが違うので調べてみたら、最近の Perlwhile 文と for 文には defined テストを自動的にやってくれる仕組みがついているようだ。知らなかった。


マニュアルのその部分を読めば「へぇー」「便利ぃ」と、この機能を好意的に迎えることができるのだけど、マニュアルを見るまではどうも納得がいかなかった。if 文のカッコの中と while 文のカッコの中で、同じ式の真偽が場合によっては違ってくるってのはやっぱり大きなことだから。


マニュアルのその部分の日本語訳を探したのだけれど見つからなかったので、へぼいけど翻訳してみた。
ここに載せたのは、man perlop あるいは perldoc perlop と打つと出てくるマニュアルの一部分。

Ordinarily you must assign the returned value to a variable, but there is one situation where an automatic assignment happens. If and only if the input symbol is the only thing inside the conditional of a "while" statement (even if disguised as a "for(;;)" loop), the value is automatically assigned to the global variable $_, destroying whatever was there previously. (This may seem like an odd thing to you, but you'll use the construct in almost every Perl script you write.) The $_ variable is not implicitly localized. You'll have to put a "local $_;" before the loop if you want that to happen.

通常は、あなたは戻り値を変数に代入しなければなりませんが、自動的な代入が起こる場合がひとつあります。while 文 (と "for(;;)" 文) の条件のところにあるのが入力演算子だけ、という場合に限り、値はグローバル変数 $_ に自動的に代入されます。$_ に入っていたものは、なんであろうと破壊されます。(これは奇妙に思えるかも知れませんが、これからあなたが書くであろう Perl script のほとんど全部で、この構文を利用することになると思いますよ。) 変数 $_ は、そのままでは local されませんので、もしそうしたいのならループの前で "local $_;" とする必要があるでしょう。

注: "(even if disguised as a "for(;;)" loop)" どう訳せばいいんだろう。for ループに変装した while ??... たんに「と "for(;;)" 文」とした。
注: 「これは奇妙に思えるかも知れませんが」の「これ」は、「破壊され」ることではなくて、「自動的に代入され」るほうにかかると思うのだけど、そこをうまく表現できなかった。
注: "if you want that to happen." の "to happen" がなんなのかよくわからなかった。"if you don't want to happen." のまちがい?

The following lines are equivalent:

    while (defined($_ = <STDIN>)) { print; }
    while ($_ = <STDIN>) { print; }
    while (<STDIN>) { print; }
    for (;<STDIN>;) { print; }
    print while defined($_ = <STDIN>);
    print while ($_ = <STDIN>);
    print while <STDIN>;

This also behaves similarly, but avoids $_ :

    while (my $line = <STDIN>) { print $line }

以下のそれぞれの行は、全部おなじことです:

    while (defined($_ = <STDIN>)) { print; }
    while ($_ = <STDIN>) { print; }
    while (<STDIN>) { print; }
    for (;<STDIN>;) { print; }
    print while defined($_ = <STDIN>);
    print while ($_ = <STDIN>);
    print while <STDIN>;

次の例も同じ様に振る舞いますが、$_ を使うのを避けています:

    while (my $line = <STDIN>) { print $line }

In these loop constructs, the assigned value (whether assignment is automatic or explicit) is then tested to see whether it is defined. The defined test avoids problems where line has a string value that would be treated as false by Perl, for example a "" or a "0" with no trailing newline. If you really mean for such values to terminate the loop, they should be tested for explicitly:

    while (($_ = <STDIN>) ne '0') { ... }
    while (<STDIN>) { last unless $_; ... }

In other boolean contexts, "" without an explicit "defined" test or comparison elicit a warning if the "use warnings" pragma or the -w command-line switch (the $^W variable) is in effect.

これらのループの構文では、代入される値は(自動的な代入だろうと明示的な代入だろうと)まず defined かどうかを見るためにテストされます。この defined テストは、行の中身が Perl によって偽とみなされる文字列だったとき(例えば、改行文字で終わっていない "" や "0")の問題を避けるのに役立ちます。

もしあなたがそのような値でループを終了させることを本当に意図するなら、明示的に値をテストするべきです:

    while (($_ = <STDIN>) ne '0') { ... }
    while (<STDIN>) { last unless $_; ... }

ほかのブール値コンテキストでは、明示的な "defined" テストを伴わない "" や、比較されていない "" があらわれた場合、"use warnings" プラグマか -w コマンドラインスイッチが有効になっていれば ( $^W 変数) warning が表示されます。

注: "for example a "" ..." とあるけれど、行入力演算子で、改行文字で終わっていない "" (空文字)が入力されてくることってあるっけ?

「ほかのブール値コンテキスト」の実験

warning.pl というスクリプト:

   1 #! /usr/bin/perl -w
   2
   3 open $in, "<warning.pl" or die; # 自分自身を開き
   4 if ($_ = <$in>) {               # 一行目を読んで、
   5     print;                      # 表示
   6 }
   7 close $in;

実行:

%perl warning.pl
Value of <HANDLE> construct can be "0"; test with defined() at warning.pl line 4.
#! /usr/bin/perl -w
%

コマンドラインでは -w オプションをつけていないけれど、スクリプトの一行目に書いてあるのでそれが効いてる。