読者です 読者をやめる 読者になる 読者になる

怖くないwhile

※おもにPerlの話です。

非エンジニアながら趣味でプログラミングに入門したのが2013年の夏。そろそろ4年になろうとしているけど、Perlの基礎を学びはじめてからつい最近まで、一貫して怖かったのがwhile文だった。

怖い、というのも変な気がするが、ようはすぐに無限ループするのがつらく、それを避けたいがためにwhileを使わずに済むかぎりはなるべく使わないようにしてきた、ということ。

すぐに無限ループするとか、なるべく使わないようにする、とかいうのは、結局whileの構造というか、挙動をよく理解していないからそうなる。

理解したくないわけではないのだけど、触るとビビッと痛みを味わい、そこから回復するまでにけっこう時間がかかるので、限られた時間を使ってプログラミング学習をしている身としては、やはり距離を置かざるをえなくなっていた。

べつに無限ループしたって、Ctrl-c とかで抜けちゃえばいいじゃん、と思う人もいるかもしれないが、ぼくは普段からちょっとしたコードの実行ならVimのquickrunというプラグインを使っているので、これで無限ループをするとVimごと強制終了&再起動しなくてはならず、ときにはPerlのプロセスが走り続けてマシンのメモリがゼロになってしまい、マシン自体の再起動もできないまま電源ボタン長押しでマシンごと強制終了、みたいなことも幾度となく繰り返してきたので、やっぱりそれはなかなかつらい。
(そのうちにそこまで行く前にプロセスを切断する方法も覚えたけど、それはまた別の話)

じゃあwhileを使わないでどうしていたのかというと、ずっとfor文を使っていた。

ここで言うfor文というのはPerlでよく使われるそれで、だからC言語なんかで使われるそれではなく、foreachのエイリアスとしてのそれである。

forはいい。forは無限ループしないから。
いや、させようと思えばさせられるけど、普通に使えばまずしない。

無限ループしないループは人に優しい。

しかしながら、forでは実現できない繰り返しというのもやはりあって、それで仕方なく時々whileに付き合ううちにようやくわかったのが以下の本題。

forは「流れ」 whileは「繰り返し」

forもwhileも「繰り返し」としてひとまとめにされがちだが、実際は全然違う。

上で無限ループについて言ったように、同じことを両方にさせることも可能だが、元々備わっている性質が違うということ。

forというのは、たとえてみれば高速道路の料金所のようなものである。
ひとつのチェックポイントを上りから下りへ(あるいはその逆へ)、多くの車が次々と通過していくが、道はワンウェイで、同じ車が何度も同じ料金所を通ることはない。

だから、これを「繰り返し」と表現するのは厳密にはおかしい。

forを通して起きている現象は本来「流れ」とでも表現すべきもので、上では高速道路にたとえたが、より汎用的に言えば「滝」のようなものか。滝はいつも同じ姿をしているように見えるが、実際には同じ水は二度と同じ場所を流れない。

一方のwhileはまさに繰り返しで、同じものが同じところを何度も走る。
それはたとえてみれば競馬とか、F1とか、あるいは陸上競技の1万メートル走とか、そういう感じ。
同じ馬や車や人が同じコースを何周も走っている。

具体例で見てみよう。

forをforらしく使うには、「最初に用意したデータをすべて通過させる」という用途で用いるのがいい。

たとえば、10個の文字列の語末に一律で「!」をくっつけるとか。

#!/usr/bin/env perl
#
# for.pl

use strict;
use warnings;
use feature 'say';

my @arr = qw/aaa bbb ccc ddd eee fff ggg hhh iii jjj/;

for my $str (@arr) {

    say $str.'!';

}

f:id:note103:20170508124043g:plain

ここでは、最初に用意した @arr の中身を処理し終えたらプログラムも終わる。
プログラムの目的は、@arr の中身(aaa〜jjj)を対象として、それらに何かすること。

一方、whileがやることは根本的にそういうものとは異なる。
これの特徴を一言で言うなら、「やめろと言うまで繰り返し続ける」ということになる。

誰かがどこかで「もうやめてください」と泣いて懇願するまで、それは同じものに対して同じことを何度でも実行し続けるし、逆に言えばそれをやめさせる条件をちゃんと設定しておけば、おとなしくそこでやめる。

whileで無限ループが生じてしまう原因の大半は、結局のところ、この「やめる条件」を設定できていないことに尽きるだろう。

そして、なぜそれを設定できていないのかと言えば、それはfor文と同じように「一連の処理を通して元のデータになんらかの変更を加える」ことを強く考えてしまっているからで、相対的に「いつ・どういう条件でやめさせるか」ということにまで想像が至ってない(考えるのが後回しになってる)からである。

しかしここまでの話を見ればわかるとおり、whileを使う上で最初に考えるべき&最も重要なことは、「いつ・どういう条件でやめさせるか」の方である。
先にそれさえ決めれば、無限ループは生じない。

whileの特徴を示すには、対話型のプログラムを作るのが良いだろう。

#!/usr/bin/env perl
#
# while.pl

use strict;
use warnings;
use feature 'say';

print '>> ';
while (my $input = <STDIN>) {

    chomp $input;

    if ($input eq 'stop') {
        say 'Bye';
        last;
    }
    else {
        say $input;
        print '>> ';
    }
}

f:id:note103:20170508124100g:plain

もちろん、というか確かに、というか、whileでも先にfor文で挙げたような処理を行うことはできる。

#!/usr/bin/env perl
#
# while2.pl

use strict;
use warnings;
use feature 'say';

my @arr = qw/aaa bbb ccc ddd eee fff ggg hhh iii jjj/;

while (my $str = <@arr>) {

    say $str.'!';

}

f:id:note103:20170508124121g:plain

しかし、これならwhileを使う必要はない。というか、forを使う方シンプルだし、目的に適っている。

また逆に、for文で先のwhile文のような対話型処理を行うことも可能ではある。

#!/usr/bin/env perl
#
# for2.pl

use strict;
use warnings;
use feature 'say';

print '>> ';
for (;;) {

    my $input = <STDIN>;
    chomp $input;

    if ($input eq 'stop') {
        say 'Bye';
        last;
    }
    else {
        say $input;
        print '>> ';
    }
}

f:id:note103:20170508124144g:plain

しかしこれまで数年間Perlを触っていて、こういうコードを見たことはないし、今後もあまり見る機会はないように思える。

よって、一般的にはfor[eache]文は「垂れ流し型」、while文は「対話型」の用途で使われる(使われやすい)という前提で話を続ける。

「限定40食の蕎麦屋」と「気まぐれなラーメン屋」

必要なのは、ありありとしたイメージである。

forとwhileの違いを「ありありと」イメージできなければ無限ループは止まらない。

forとwhileの違いは、「限定40食の蕎麦屋」と「気まぐれなラーメン屋」の違いとして想像することができる。

「限定40食の蕎麦屋」は、for文のたとえである。以下のようなコードになる。

#!/usr/bin/env perl
#
# for_soba.pl

use strict;
use warnings;
use feature 'say';

my $limit = 40;

for my $count (1..$limit) {

    say "soba $count";

}

f:id:note103:20170508124211g:plain

最初に限定食数を変数 $limit に設定して、それを売り切ったら終わりである。

一方のwhile文は、「気まぐれなラーメン屋」にたとえられる。
コードにすると、このようになる。

#!/usr/bin/env perl
#
# while_ramen.pl

use strict;
use warnings;
use feature 'say';

print 'time >> ';
while (my $input = <STDIN>) {

    chomp $input;

    my $open = 11;
    my $close = $open + int(rand 12);

    say "Opening hours is $open - $close.";
    if ($input < $open || $input > $close) {
        print 'time >> ';
    }
    else {
        say 'ramen';
        last;
    }
}

f:id:note103:20170508124232g:plain

ここの店主は午前11時に店を開けるが、何時まで営業するかが日によって違う。店主の気まぐれですぐに閉めたり、22時まで営業したりする。

客は自分の行きたい時間を「13」とか「18」とか入力し、開いていれば「ramen」にありつけるが、開いてなければ開くまで時間を入力し続けることになる。

whileはこういう処理に向いている。

言い換えると、forは「何を(あるいは何回)実行するのか事前にわかっている時」に使いやすく、whileは「何回実行するのか事前にはわからない」という状況で使いやすい。

そしてそのようなwhileでは、「何回繰り返すのか」が事前には決まっていないから、自分で能動的に「やめどき」を作らなければならない。

終わりに

この記事のきっかけになったのは、BSジャパン(テレ東のBS版)で毎週放送されている『ワタシが日本に住む理由』という番組で、バングラデシュ出身の日本蕎麦屋さんの回を見たことだった。

腕の良い&こだわりのある彼は、毎朝40食分の蕎麦を打ち、それが売り切れたらその日の営業は終わってしまう。

番組じたい面白かったが、それを見ながら、「これはfor文みたいだな」とふと思った。

そして、これがfor文ならwhile文は何だろう? と考えて上のような話になった。

for文は「繰り返し」ではない。「流れ」である。
手元に溜めておいた素材を出しきったら、そこでおしまい。

たしかに見た目には「回っている」ように見える。というより、システム自体は回っているが、その上を流れるデータは回っていない。

それは新聞などを印刷する輪転機のようでもある。
輪転機はグルグルと回り続けているが、その上を流れる紙は長い1本道をスタートからゴールまで駆け抜けているだけである。

while文は「繰り返し」とも言えるが、本質はその「終わらなさ」だと思う。

それは『うる星やつら劇場版 ビューティフル・ドリーマー』のように、あるいはビル・マーレイ主演による『恋はデジャ・ブ』のように、同じ日々を何度も繰り返す。

どこかに決定的な「終わりへの抜け道」を作らなければそこから抜け出すことはできず、それが実現するまではひたすら終わらない1日を繰り返さなければならない。

whileはやっぱり怖い。