前回はインデントに関する話の途中で終わってしまったので、早速続きを。 前号で紹介した(CD-ROMに入っている)リストからインデントが1段の行を抜き出すとList 1のようになる。 え、何の話だって?
いきなり続きと言われても…、前号を見ていないと確かに困るでしょう。 NIFTY SERVEのFPROGORGとか私の個人ページで連載した内容を紹介しているので、そちらをご覧ください。
前号を読んだが忘れた、という場合が問題だ。 そりゃ、1ヵ月前に見たリストを覚えている人がいるとしたら、かなり記憶力がいいか、他にソースを何も見ていないか、多分このどちらかだと思う。 自分が書いたプログラムならともかく、このようなコラムに出てきたリストを一ヵ月も覚えているのは奇妙である。 だいいち書いている本人も半分忘れていたりするのだ。
余談だが、時間が経過するほど忘れるという法則は、長期経過した場合だけでなく、かなり短期においても成立するようである。 だから、プログラムを書く時に、関連する処理は近くに書くのが鉄則だ。 「今読んでいる」内容は、しばらく覚えているので、忘れないうちに関連ある処理を見せるようなソースを書くのである。
つまり、「もしアレだったら(1)だけど、そうでなければ云々…」という文章を考えてみる。 これなら「そうでなければ」という条件が「アレだったら」とは別のケースを意味すること、すなわち「アレでなければ」という意味であることが分かりやすい。 「(1)だけど」という内容をサラっと読んでいる間、頭のどこかに「アレだったら」ということがキャッシュされているのだ。
これに対して「もしアレだったら云々…(すごく長い内容)…だけど、そうでなければ(1)です」とか言われると、「そうでなければ」と言われた頃には「アレだったら」ということがキャッシュからクリアされていて、何のことだか分からなくなってしまうのである。 この場合は「もしアレでなければ(1)だけど、アレなら云々…」という順序にすれば、短い処理が先に来るので、脳内のキャッシュメモリが少なくても処理ができる。
とりあえず、何もないというのもあんまりなので、前回のポイントだけ書いておく。
インデントの目的はソースを見やすくすることで、具体的な効果は二つある。
というわけで、List 1である。
---- List 1 ---- |
cgi_header("counter"); for (;;) { cgi_footer(); |
List 1って何? インデントが1段の行を抜き出したんですよ。 最初に書いたでしょう。 間に別のことを書くと分かりにくいという実例でした。 でもこの程度なら覚えている人も多いかもしれないが。とりあえず、折角元のリストを忘れているのだから、これだけで何をする処理か想像してみる。 なにか分からない(^^;)。 強引に解釈してみれば、cgi_header()という関数を呼び出し、forのループを実行し、何かの条件でループが終了すればcgi_footer()という関数を呼び出す、という一連の流れがある、ということか。 そのまんまだが。しかし、もし、この3つの処理が、実際の処理の流れにきちんと対応しているなら、これで全体の処理を概観したことになる。 特定の位置に注目すれば全体を把握できる、これがまさにインデントの威力なのだ。
ところで、CGIのことを知っていれば、cgi_header()とかcgi_footer()という処理が何となく予想できるかもしれない。 これは名前の付け方の妙味だろうか。 header()、footer()という関数名でもよさそうだが、たまたま「cgi_」という表現が付いているのが、1ヵ月後になって効いてくるわけだ。 名前を付ける時にケチるなという教訓である。 とはいっても「CGIのfooterって何?」とか考えてみると何かよく分からないかもしれませんが。 それに何でもかんでも長い変数名にすればよいものでもないので、誤解なきよう。
次に、元のリストからインデントが2段になっている個所を抜き出すと、List 2のようになる。 実はこれはインデントが1段の処理中のforの内部に対応している。
---- List 2 ---- |
if (create_counter_path()) if (create_command_name(cmd)) if (read_counter()) if (message_type >= 0) { update_counter(); break; |
4つのifとupdate_counter()、break、という処理の流れになっている。 breakが最後にあるforループというのも妙だが、この技法は前号に書いた通りなので繰り返さない。
この4つのifのうち、3つまでが条件式を入れる個所に関数呼び出しを割り当てている。 つまり、条件判断を別関数で行なう、という仕組みだ。 isasiiのような標準関数の使い方の真似である。 別に珍しいテクニックではないが、このような場合は関数名をケチらないで、何を判定しているかを明確に表現した方が、プログラムを把握する手助けになることが多い。 create_counter_pathという名前はやや長すぎるきらいがあるが、短縮した名前を使って、それを補うコメントを付けるよりは、名前そのもので判別できた方がいいんじゃないか、という発想である。 もちろん、これには異論があると思うし、counter_path()が何だとかcommand_nameって何ということは分からないわけで、それに関しては別途説明が必用になるわけだが。
さて、これらのifを見ると、判定の関数を呼び出して、結果が真すなわち非0だったら何か処理するのだろうか、と思うわけだが、実はプログラムはList 3のようになっている。
---- List 3 ---- |
if (create_counter_path()) break; |
つまり、結果が真だったらそこでforループから出てしまうのだ。 ということは、この関数は何かをテストして失敗した時に真を戻す仕様になっていたのだ。 ifの所だけ見ると、ちょっと意表を突いた書き方であることが分かるが、実際はbreakが即座に続いて現れるので、それほど違和感がないと思う。 抽象的に考えれば、各種のテストで選別して最後まで通った場合のみ何か処理する、というような流れと言えるだろう。
さて、List 2もif-breakが続いている所はそれで終わりの処理で、さらに奥深い処理はmessage_typeが0以上の値の時の処理だけである。 インデントが3段の所だけを抜き出してみるとList 4のようになる。
---- List 4 ---- |
if (counter == 0) { } else { } |
これはまたシンプルだ。 counterが0の時とそうでない時で別の処理をしている、ということが分かる。というか、それしか分からないのだが。
またまた余談だが、同様の比較を行なう書き方として、List 4'のようにすることも考えられる。
---- List 4' ---- |
if (counter != 0) { } else { } |
どちらがよいだろうか? これはまたどっちもどっちでよく分からない。 「0でない」という判定は、それを省略して書けるという特徴があることはあるが、かといって「!= 0」を略してもよいかという問題を増やすのもいまいちかもしれない。
判断を保留して、先にインデントが5段を行を見てみると、ifの条件が成立した場合にはprintfを実行する1行だけになっている。 それに比べて、elseから後の処理は結構長い。 こういう場合は、処理が短いものを先に書くというのが分かりやすい。 ということを考えると、この場合は短い処理が先に来るようにList 4の書き方を選択した、ということが分かる。 先ほど話題になった脳内キャッシュの応用である。何の話だって?
インデント5段はList 5の3行である。
---- List 5 ---- |
if (message_type > 0) printf(" %ld ", counter); if (message_type > 0) { |
message_typeが正の数の時の処理があって、途中に無条件で実行する処理が挟まっているのだから頭が痛い。 当然、list 5'のようにも書けるはずである。
---- List 5' ---- |
if (message_type > 0) { ... printf(" %ld ", counter); ... } else { printf(" %ld ", counter); } |
ifの条件判定をダブらせるか、同じprintfを二度書くか。好みの問題かもしれないが、全体としては一貫性があった方がいい。 実際はそこまで考えずに直感で書けるようになっていなければいけない。
さて、インデント6段はList 6のようになる。 このあたりになると、処理の細部ということで、全体を把握したい場合には真剣に読まなくてもよいレベルだと思う。
---- List 6 ---- |
if ((counter % 1000) == 777) { } else if ((counter % 1000) == 666) { } else if ((counter % 10000) == 0) { } else { } |
ここまで来れば何をやっているか思い出すわけだが、カウンターが10000の倍数になった時だけに表示されるメッセージがあるらしい。 自分で作ったのに完全に忘却しているようだ。
以上のように、インデントのレベルが同じ処理を追いかけることで、処理の流れの概略が把握できるような構成になっていることが分かる。
プログラムの内部がそれほど整然としている保証はどこにもない。 インデントと処理の流れを対応させないように書けば、いくらでも奇妙なプログラムが出来る。 List 1では、cgi_header()という処理と、forのループが、同一の位置付けの処理となっているように見えるが、それは単にそのように見えているだけである。 あくまでインデントというのは表面的な装飾に過ぎない。 実際に処理をインデントに対応するように書くのはプログラマーの責任なのである。
しかし、そう堅い事を言わなくても、C言語のような構造化を前提にしたプログラミング言語を使うのであれば、普通に書けば同じレベルの処理が順番に現れるような書き方になるものだ。 これは、その方が書く側にとっても分かりやすいからである。 自分が分かるように書けば、自然にそうなるし、少し意識していればなおさらである。 もしこれができないのなら、その人が別のパラダイムの言語に相性がいいとか、あるいはプログラミングそのものに向いていないとか、そのような状況を考えた方がいいかもしれない。
ところで、インデントといえば、何文字分の段を付けるのがよいか、というのが宗教論争のようになっているからその話題にも触れておこう。 K&R 1st.では5文字、2nd.では4文字であることはどこかで紹介したかもしれない。 GNU系のソースは2段のものが多いようだ。 私の場合、タブ1つ、というのも割とよく使う。 これはソースを見る画面によってインデントの幅が変化する。
要するに、インデントの段が違う場合に明確に区別できればよいのだから、1文字というのは論外としても、2〜4文字というのは妥当な所かもしれない。 結論としては、処理の流れが追えるように段が付いていて、まとまった処理は塊に見えるように、という書き方なら、それほど深く考えなくてもいいんじゃないかと思う。
(フィンローダ ニフティサーブ FPROG SYSOP)