バグのないプログラムの書き方

突然であるが、プログラムにはバグが付き物だ。
それゆえに、統合開発環境にはGUIデバッガがついていたりして
プログラムの処理が追いやすくなっていたり、
テストケースを書いたりして部分的にバグの発生を制御したりする。


だが、それは本当だろうか?
もっと別の方法でバグの発生自体を抑制できないのだろうか。
ここのところ私はこのことばかりを考えていた。
Haskellをはじめたのもこのためだ。
そしてこれは私の中で、そろそろ結論に達しつつある。
(うーん。こんな深遠なテーマに軽々しく結論などと言っていいものか)


それで、結論から言えば、バグを抑える方法があるのは間違いない。
実際に最近私が書く(C++の)コードのバグ発生率は以前と比べて
著しく低下した。その変化っぷりに自分でも驚くことしばしばなのだ。
やはりなにかコツがあるのだろう。
では、そのコツとはなんだろうか?


私の各コードのバグが少なくなった時期を考えてみると、
どうも、というか、やはりというか、Haskellを勉強していた時期とかぶるようだ。
HaskellでのコーディングスタイルがC++でのコーディングにも影響を与え、
その結果バグの出にくいコードが書けるようになったんだろうか。
しかし、これはあまりにも抽象的である。
もっと具体的に何がどのように変わるのか?
それが分かればバグのないプログラムを書くための一助となるかもしれない。


これに対する私の見解であるが、一言で言ってしまえば、
「無理をしない」である。
あらゆることについて無理をしない。
無理をせずに自明なコードだけ書いていればバグの入る余地がなくなる。
Haskellをやりだしてから細かいことを書かなくなった
(書きたくなくなった)。やりたいことを書けばいいのである。
これはある意味で富豪的プログラミングに通じるのであるが、
今回の話は富豪的というところにとどまらない。
徹底的に首尾一貫して手を抜けというのである。


逆に言うと、手を抜くために思慮深くなれ、ということだ。
プログラムを書く前に、ちょっと待った。
ゆっくりと考えを巡らせてもっと簡素にかけないかを考える。
ここで言う簡素というのはプログラムの長さではない。
コード化する際に、いかに考える余地が入らないかである。


これでもあまりに抽象的である。
具体的に考える余地を減らすガイドラインを考えてみる。
HaskellC++での違いであるが、やはりもっとも大きな違いは
純粋関数的ということだろう。
プログラムが純粋関数的であるならば、
入力により出力が決まるのでバグの入る余地は圧倒的に
少なくなるはずである。
その最たるものとして、変数に代入を許さない、
これをC++でもなるべく守るようにする。
もちろん、C++で変数に代入をせずにプログラムを作るのは
とても難しい。だから、完全に排除するのではなくて、
代入によってプログラムの見通しが悪くならないように心掛ける。
プログラムの離れた場所で同じ変数に代入を行わないとか、
変数の内容が掌握しやすければしやすいほど
プログラムから考える余地は減ると思う。
同じような理由で変数の生存区間を小さくするのも有効だろう。
同じ変数をいろいろなところで使わないのも重要だ。


もっと直接的な関数的な考え方としては、
再帰的なアルゴリズムを繰り返しに変換しない、というのがあるだろうか。
再帰的な関数と繰り返しに変換したものでは
たいてい再帰的な関数のほうが理解しやすく掌握しやすい。

int gcd_r(int a,int b)
{
  if (a%b==0) return b;
  else return gcd_r(b,a%b);
}

int gcd_i(int a,int b)
{
  while(a%b!=0){
    int c=b;
    b=a%b;
    a=c;
  }
  return b;
}

たとえば、GCDなら、上の再帰版はまったくの定義の通りで、
正しいことは自明であるが、
繰り返し版は注意深く読まないと正しいことが分からない。
速度の問題があるかもしれないが、繰り返しに変換できる
再帰関数ならばせいぜい定数倍である。
本質的に問題となるのは、呼び出し回数があまりにも多くなったとき
スタックがあふれるかもしれない、ということであるが、
そのときは確かに仕方がない。
しかし、通常スタックは数MB存在するので、
スタックフレームで呼び出し一回あたり数十B消費されたとしても
十万回は呼び出せる計算になり、このオーダーの再帰回数が
必要になるアルゴリズムはなかなかない。
(あるいは、よくできたコンパイラなら末尾呼び出しの
最適化を期待できるかもしれない。VCでは末尾再帰呼び出し
だけなら何とかがんばってくれていたような気がする。
まぁ、最適化してくれないものと考えておくのが正解かもしれないが)
ほかには、DPを実装するとき、これを元の再帰的なアルゴリズム+
キャッシュと実装するほうがテーブルを組織的に埋めていくよりも
楽な場合が多い。テーブルを組織的に埋めるには
添え字の問題や順序など、いろいろと神経を使うことが多い。
やはり考慮すべきことは少なければ少ないほうがいい。


後は、最近では当たり前であるが、
配列や文字列を値として扱う、つまりC++であっても
配列をポインタで持たないということだ。
(配列はともかく)文字列をポインタで持つとバッファ長など
これまた色々と神経を使う。要するにコンテナを使えということだ。


とまぁ、(いろいろと考えては見たものの)
あんまりたいしたことは書けなかったが、
やはりそういうコツがあるのは間違いない。
感覚的にそういうのがあるのは分かるのだが、
文章にするのはとても難しい。
今回、言いたいことがほとんど書けていないのだが、
これだけは言える。関数型言語を勉強したことにより
そういう考え方がC++でのコーディングにも影響を及ぼしているのだ。
(C++Haskellも使う方で、同じような考えの人は居られますかね…?)