FPROGORGで、プログラムのソースが可変幅のフォントでプリントされていると違和感があるか、という話題があった。 K&Rを筆頭として、C言語系のテキストは固定幅でソースリストが印刷されているものが多い。Cマガジンも同様である。 これに対して、Pascal系のテキストはプロポーショナルで印刷されていることが多い。なぜだかよく分からない。歴史的な理由があるのだと思う。 ちなみに、Cマガジンでもリストではなく本文中にプログラムの一部を引用する場合にはプロポーショナル、つまり文字幅が文字によって異なるような印刷になっているようだ。
昔はCのソースのインデントを空白5個分にするとか4個にするとか、2個あれば十分だとか、TABは空白8個だとか、各種論争の種があったものだが、最近このような議論をあまり見た記憶がない。 可変幅のフォントが使われることが多くなったのかもしれない。 固定幅のフォントを使わないと、空白いくつと言われても、実際にどの程度の幅になるのか分からないのだ。 プログラムのソースは固定幅のフォントで表示するという規則はないのである。
ところで、他の人のソースを見ると、どうにも奇妙なインデントを付けている人がいる。なぜインデントするのか、というのも分かっていないからだと思う。
なぜインデントするかというと、当然、その方が見やすくなるからだ。 見やすいというのは重要なことで、その結果、バグが減るだけではない、アルゴリズムも改善される可能性まであるのだ。 Cはフリースタイルだからどんな書き方をしても結果は同じと思われるかもしれないが、確かに空白をどう使ってもコンパイラが出すコードには関係ない。問題は人間の側にある。 全体が見通せるような書き方になっていれば、それだけ他のことに集中する余力が生まれるし、凡ミスも避けられる。 ここが重要なのだが、その結果、プログラムの品質そのものが良くなることにつながるのである。 よいプログラムは見た目にも奇麗に書かれていることが多いはずだが、これは決して偶然ではなく当然の結末なのである。
統合環境のエディタだと、予約語を強調表示にするとか、コメントをイタリックにするとか、表示にさらなる工夫をこらした製品もある。 これも単に見た目が奇麗というだけでなく、プログラムを読む時の誤解を減らすという意味が重要なのだ。
インデントの目的がソースを見やすくすることだとして、具体的にはどのような効果を狙えばよいか。インデントの効果は二つある。 関連のある内容をまとめることと、同じレベルの処理を識別しやすくすることである。 後者は説明が難しいのだが、こんなのはどうか。Fig.1は買い物メモである。
---- Fig. 1 ---- |
三丁目の商店街に行って、 八百屋が開いていたら、 ねぎを買う。 にんじんを買う。 安ければキャベツを買う。 大根を買う。 魚屋が開いていたら さんまを買う。 |
まあ分かりやすいメモであるが、大根は安くなくても買ってよいのか、というような問題があったりする。 さんまを買って大根がないと悲しいぞという話はともかくとして、八百屋で何を買うとか魚屋で何を買うか。 インデントするとFig. 2のようになる。
---- Fig.2 ---- |
三丁目の商店街に行って、 八百屋が開いていたら、 ねぎを買う。 にんじんを買う。 安ければ キャベツを買う。 大根を買う。 魚屋が開いていたら、 さんまを買う。 |
左端から書いてある行が3つある。 表現は微妙に違うが、商店街に行く、八百屋に行く、魚屋に行く、という3つの行動が要求されていることが、インデントしたことにより明白になっている。 これはインデントが「インデントされなかった個所を強調する」という効果を持っていることかを意味する。 逆に考えると、インデントというのは「細かいことはちょっとこっちに置いといて…」という処理なのである。 見た感じでも、何となく「八百屋に行く」と「魚屋に行く」が同列(同桁か?)であるという雰囲気があるだろう。 箇条書きにする時に、
・行の左端に何かマークを付ける。 ・数字を使うこともある。
のようなスタイルがある。 マークや数字を付けることかによって、それらが付いている行が同じレベルであることを表現しているのだ。 インデントというのは、そのようなマークの代わりに「空白」という文字を用いて同じレベルを表現する手法なのである。
もう一つの効果も考えてみる。 つまり、関連するものがまとまって見えるということである。 インデントしたことによって、Fig.2はFig.3のような固まりから構成されていることが分かりやすくなっている。
---- Fig.3 ---- |
┏━━━━━━━━━━┓ ┃ ┃ ┃ ┌───────┐┃ ┃ │ │┃ ┃ │ │┃ ┃ │ ┌───┐│┃ ┃ │ └───┘│┃ ┃ └───────┘┃ ┃ ┃ ┃ ┌───────┐┃ ┃ └───────┘┃ ┗━━━━━━━━━━┛ |
実際はこれでもまだ不十分だ。簡単な改善方法が知られている。 空白行を使うのである。 認知心理学の研究では、空白で分断することによって、個々の固まりがまとまったものとして認識されることが知られている。 「なにもない」という状態を使うことで、「ある」ものが強い結合を持つ結果になる、というのが面白い。 難しいことを考えなくても、空白を使うことによって、それで分断されたそれぞれの集団が「まとまった状態になる」ことは容易に想像できるだろう。 つまり、Fig.4のように書けば、全体が3つのパートから構成されているように見えるはずである。
---- Fig.4 ---- |
三丁目の商店街に行って、 八百屋が開いていたら、 ねぎを買う。 にんじんを買う。 安ければ キャベツを買う。 大根を買う。 魚屋が開いていたら、 さんまを買う。 |
もちろん、このように書くためには、処理の内容が「関連するものがまとまっている」という状態でなければなりませんが。 何も考えないとかえって関係したものが自然にまとまるものである。 まとまった考えが難しいという人は、もしかすると芸術家に向いているかもしれない。 もっとも、実際に処理がまとまった流れに書けなくなる理由は、一旦作ったプログラムを後から修正するからである。 ツギハギの状態になってしまうのだ。
以上の理屈を頭に叩き込んでおけば大抵の応用は自然に出来るはずだが、実際の例は私のヘボいプログラムを使ってみよう。 List 1は、本誌CGIの特集の時に使ったプログラムである。
---- List 1 ---- |
static void cgi(char *cmd) { cgi_header("counter"); for (;;) { /* 単なるblockにしないのは、途中でbreakできるようにするため */ if (create_counter_path()) break; if (create_command_name(cmd)) break; if (read_counter()) break; /* カウンタがあればカウントアップされている * カウンタがない場合は1になっている * カウンタがoverflowしたら0になっている */ if (message_type >= 0) { if (counter == 0) { printf("???"); } else { if (message_type > 0) fputs(message_kokoni, stdout); printf(" %ld ", counter); if (message_type > 0) { if ((counter % 1000) == 777) { fputs(message_ninmedesu, stdout); fputs(message_777, stdout); printf("\n<br>\n"); } else if ((counter % 1000) == 666) { fputs(message_ninmedesu, stdout); fputs(message_666, stdout); printf("\n<br>\n"); } else if ((counter % 10000) == 0) { fputs(message_ninmedesu, stdout); fputs(message_just, stdout); printf("\n<br>\n"); } else { fputs(message_ninmeno, stdout); } } } } update_counter(); break; } cgi_footer(); } |
ところで、このプログラム、今見直してみると穴だらけである。 まず"???"というのが無茶苦茶怪しい。 trygraphというのは殆ど使われていないという説もあるが、ANSIの正式の規格なんだから無視するわけにはいかない。 しかし個人的にはこれを本来の意味で使ったことなど一度もない。 すると、無視したわけではないが、ついうっかり忘れてしまうという事故が発生する。 コンパイル時にメッセージをよく見ていれば分かる程度の問題なのかもしれないが。
もっと怪しいのはforのループだ。 一見すると無限ループのような書き方だが、実際は1度もループしていないのである。 コメントにも「単なるblockにしないのは、途中でbreakできるようにするため」と書いてある。 このコメントがないと謎のプログラムになりかねない。 breakに関係する所を抜き出すとList 2のようになる。
---- List 2 ---- |
for (;;) { if (create_counter_path()) break; if (create_command_name(cmd)) break; if (read_counter()) break; /* 途中略 */ break; } |
breakを使わずにgotoでも使うことにすれば、List 3のようになる。
---- List 3 ---- |
if (create_counter_path()) goto finish; if (create_command_name(cmd)) goto finish; if (read_counter()) goto finish; /* 途中略 */ |
---- List 4 ---- |
if (create_counter_path() == 0) { if (create_command_name(cmd) == 0) { if (read_counter() == 0) { /* 途中略 */ } } } |
---- List 5 ---- |
if (create_counter_path() == 0 && create_command_name(cmd) == 0 && read_counter() == 0) { /* 途中略 */ } |
---- List 6 ---- |
if (create_counter_path()) { fprintf(stderr, "pass: create_conter_path\n"); break; } |
これでは&&で条件を連結するのが難しい。 ここで注意して欲しいのがfprintfのインデントである。 この行がインデント0段で書かれているというのはインデントの原則を破っていることになる。かなり目立つはずだ。 このような行は、プログラムを完成した時に全部取り除くことを大前提として、わざとこうしてあるのである。 インデントの使い方としては裏技かもしれないが、よく使われる技法だと思う。ページがなくなってしまったので以下次号。
(つづく)(フィンローダ ニフティサーブ FPROG SYSOP)