範囲演算子

スライスを勉強した時に「範囲構文」として勉強した「a..b」という書き方ですが、ラクダ本を読んでいたところ、これは構文というよりは「..」という「範囲演算子」によるもの、という事がわかりました。
今まで「整数と整数の間の範囲の整数リストを作り出す」という程度の認識で使っていたのですが、やはりちゃんと勉強しないとダメですね。知らなかった使い方が見つかりました。(^_^;)
というわけで、範囲演算子をちゃんと勉強したいと思います。


ラクダ本プログラミングPerl〈VOLUME1〉)によると、範囲演算子(range operator)は「コンテキスト」によってその意味が変わるそうです。
「コンテキスト」は、ぱるも日記でも何度か登場して勉強していますが、「文脈」の事ですね。スカラー変数への代入文の右辺や if 文の条件文など、「スカラー値」が必要と判断される場所は「スカラーコンテキスト」と呼ばれ、配列変数への代入文の右辺や foreach 文の対象など、「リスト値」(複数のスカラー値)が必要と判断される場所は「リストコンテキスト」になるのでした。


範囲演算子を「リストコンテキスト」で使った場合は、今までの使い方である「整数と整数の間の整数リストを生成」という意味になります。

my @onetoten = (1..10);   # 配列変数への代入なのでリストコンテキスト
print "@onetoten\n";
1 2 3 4 5 6 7 8 9 10

また、Perl には「マジックインクリメント」という機能があり、/^[a-zA-Z]*[0-9]*$/ にマッチする文字列が入ったスカラー変数をインクリメント(++)する事ができます。数字の10文字にアルファベット26文字を加えて、「36進数」であるかのように振舞うのです。ケタ上がりにも対応しています。

my @nums = qw(A ab AZ Z9 01 perl);
print "$_ -> ", ++$_, "\n" for (@nums);

@nums に入れた文字列をそれぞれインクリメントして表示します。実行結果は以下の通りです。

A -> B
ab -> ac
AZ -> BA
Z9 -> AA0
01 -> 02
perl -> perm

範囲演算子は内部でマジックインクリメントを利用しているので、範囲演算子の項には文字列を渡す事もできます。

my @atoz = ('a'..'z');
print "@atoz\n";
a b c d e f g h i j k l m n o p q r s t u v w x y z

便利ですね。(^_^)


範囲演算子スカラーコンテキストで評価した場合の動作は全く異なります。
スカラーコンテキストでの範囲演算子は、左右に真偽値をとり、真偽値を返す、論理演算子のように振舞います。しかし、ただの演算子とは違い、それぞれの範囲演算子が個別の「内部状態」(ON/OFF)を持っている、というのがポイントです。どの範囲演算子の内部状態も最初は「OFF」となっています。
例えば「A..B」がスカラーコンテキストに書かれていて、繰り返し構文などで何度も評価されるとします。
この「A..B」を評価すると、内部状態が「OFF」の時は A が評価されます。A が偽の場合、範囲演算子は何もせずに偽を返します。ですが、A が真になると範囲演算子の内部状態は「ON」となり、範囲演算子は真を返します。「A..B」が評価された時に、内部状態が「ON」の時は B が評価されます。B が真の場合、範囲演算子は何もせずに真を返しますが、B が偽になると内部状態が「OFF」になり、範囲演算子は偽を返します。

つまり、スカラーコンテキストでの範囲演算子は、「ON 条件と OFF 条件をとるスイッチ」のように振舞うのです。最初は ON 条件だけを見張っていて、一度 ON 条件が真になれば次からは OFF 条件を見張りだす、というように動くわけですね。見張られていない方の条件は「評価されない」事に注意してください。
また、ON 条件が真になった時 OFF 条件も評価されますが、ドットを3つにする(...)と ON 条件が真になっても次回の評価まで OFF 条件の評価は始まりません。


このスカラーコンテキストでの範囲演算子は、使い道が思い浮かばないかもしれませんが、色々な場面で利用できます。スイッチの代わりになる、というのが便利なのです。

while (<DATA>) {
  chomp;
  print "$_\n" if ( /^_START_$/ .. /^_END_$/ );
}

__END__
AAA
_START_
BBB
CCC
_END_
DDD

特殊ファイルハンドル DATA には __END__ 以降のテキストが入っているのでしたね。DATA に入っているテキストを行入力演算子で毎行読み込んで、範囲演算子正規表現パターンを組み合わせて判定しています。
この場合 "_START_" という行が見つかったら、範囲演算子は "_END_" という行を見つけるまで真を返すようになります。("_START_" から "_END_" まで)
実行結果は以下の通りです。

_START_
BBB
CCC
_END_

_START_ と _END_ の間のテキストだけを表示する事ができました。


また、ON/OFF 条件として「リテラルな正の整数」(0, 1, 2...)を指定した場合、暗黙のルールとして特殊変数「$.」(最後に行入力演算子で読み込んだ行の行番号)と比較されます。

while (<DATA>) {
  chomp;
  print "$_\n" if (2..4);
}

__END__
AAA
BBB
CCC
DDD
EEE

この場合の範囲演算子は「2行目から4行目」と読み替えればいいわけですね。(^_^)
実行結果は以下の通りです。

BBB
CCC
DDD

「○○から××まで」を直感的に書く事ができて、コードがとても簡潔になりますね。使いどころがちょっと難しいですが、色々と応用できそうです。