バグの少ないプログラミングの考え方の例
わたしがプログラミングをするときに、バグが少なくなるように意識していることを整理して書き出します。内容の正確さを保障できませんので予めご了承ください。
形式手法
形式手法とは数学や論理学に基づいたソフトウェア開発・検証手法を指します。プログラムと証明には対応関係があること*1が知られています。そのため、仕様を数学的に記述できれば*2、プログラムがその仕様を満たすかを検証できます。
ただし、以下の理由から数学的に仕様を記述することが最適でないケースが多いと考えれます。
- 仕様を数学的に記述することが難しい。
- 多くの分野では最適な仕様が定まらない。フィードバックにより仕様を変更していくことが主流。
大きなシステムで仕様を数学的に記述することは容易ではありませんが、数学や論理学の考え方をプログラミングに活かすことでバグを出にくい開発が可能です。
関数型プログラミング
CやJavaはプログラミングパラダイム*3としては命令型プログラミングに属します。また、数理論理学的な性質を表現するプログラムパラダイムとして、宣言的プログラミングがあります。宣言的プログラミングの中でも実用的なものとして、数学上の関数を主軸にプログラミングを記述する関数型プログラミングがあります。
関数型プログラミングの特徴として参照等価性が挙げられます。参照等価性は「式をその式の値に置き換えてもプログラムの振る舞いが変わらないこと」を指します。これは関数に副作用*4がないことを意味します。
関数型プログラミングの利点
以下が挙げられます。
- プログラムが理解しやすくなる
- 余分な状態が現れにくくなる
- タイミングの問題が起きにくくなる
プログラムが理解しやすくなる
副作用のない関数を多く書こうとすると、副作用が必要な/不要な箇所が明瞭になります。そのため、プログラムの構造がシンプルになりやすいです。
余分な状態が現れにくくなる
副作用のない関数が多くなると、関数の入出力以外の状態を操作する状況が少なくなります。そのため、状態を保持しなくても表現できる箇所が増える傾向にあります。
タイミングの問題が起きにくくなる
数学的な関数は、その関数が評価されるタイミングに依らず成り立つ性質を表します。タイミングの制御は複雑であり、命令型プログラミングではあるタイミングでうまく行っても違うタイミングでは意図しない挙動になることが多くあります。関数型プログラミングでは状態に依存しない(副作用のない)関数を扱うため、タイミングで挙動が変わるプログラムが書きにくくなります。
関数型と命令型の使いどころ
関数型プログラミングの利点は多くありますが、現実的に多く使われているプログラミングパラダイムは命令型プログラミングです。これは私たちが扱うコンピュータが命令的に(状態を変化させながら)動作しているからです。関数型プログラミング言語で書かれたプログラムも、コンパイラによって(命令的な)機械語に変換してコンピュータを制御します。
個人的所感として、関数型(宣言型)プログラミングは「人間が理解しやすい」、命令型プログラミングは「コンピュータが理解しやすい」ことが挙げられます。命令型プログラミング言語でも(言語のサポートはありませんが)副作用のない関数でプログラムを構築できます。そのため、わたしは命令型プログラミング言語を扱う場合でも関数型プログラミングをできる限り行います。可能な限り関数型プログラミングを行い、命令的に書かなければならないところだけを命令的に書くように意識しています。
命令型プログラミングが適している状況
命令型プログラミングの方が関数型プログラミングよりも適している状況を以下に挙げます。
- 状態を管理する必要がある
- 最適化の必要がある
状態を管理する必要がある
サーバーアプリケーションやGUIアプリケーションのように、動作を続けて外部との通信を行うプログラムは状態を管理する必要があります。
最適化の必要がある
実行速度やメモリ効率の性能向上は状態制御を最適化することで実現できることが多いです。
関数型プログラミングの設計
プログラミングの設計表現としてUML*5が最も使われています。しかし、UMLは状態制御やタイミングの設計が主であり、命令型プログラミングに沿った設計表現だとわたしは考えています。
関数型プログラミングの設計表現の提案
関数型プログラミングに適した設計表現としてわたしの理解では以下の2つが挙げられます。
- 型システムを用いた設計
- データフロー設計
型システムを用いた設計
例えば、純粋関数型プログラミング言語のHaskellでは、実装をundefinedとすることで、型検査のみを実行することができます。強力な型システムのサポートが得られる状態で型の設計ができます。
そのため、他の言語でもHaskellを持ちいて型の設計を行うことは有用に思います。しかし、図的表現ができないため、直観的理解が得られにくいと考えられます。
func_a :: Int -> Int func_a = undefined func_b :: Int -> Int func_b = undefined func_c :: Int -> Int -> Int func_c = undefined func :: Int -> Int -> Int func x y = let a = func_a x in let b = func_b y in func_c a b
データフロー設計
データと副作用のない関数を矢印で結んだデータフロー図を設計表現としてわたしは用いています。例えば図のような表現ができ、func_aとfunc_bは並列に処理できることが分かります。直観的理解を得やすい利点がありますが、以下のような課題があります。
- 関数を引数に取る関数を表現できない
- mapのような複数のデータ群に対して関数を適用することをうまく表現できない
プログラミング技術
プログラミングの設計技術をいくつか紹介します。
アクターモデル
並行/分散計算の数学的モデルのひとつです。メッセージパッシングの基本となる考え方といえます。他アクターの情報を把握する手段をメッセージを介してのみにすることで、データ競合の発生を防ぐことができます。
リアクティブプログラミング
データの変化に反応して、処理を実行する技術です。オブザーバーパターンを使って実装することが多いです。命令型プログラミングと関数型(宣言型)プログラミングとの切り替え点として利用するのが有用です。オブザーバーをデータストリームとみなすことができます。
契約プログラミング
処理の前処理/後処理に対する考え方です。処理を実装するときに毎回バリデーションを実装する防御的プログラミングの課題として、過剰な実装が必要になることが挙げられます。契約プログラミングでは処理に事前条件と事後条件、不変条件を定めて呼び出す側で条件に見合うようにすることで、処理の保証範囲を明瞭にします。契約プログラミング(契約による設計)はカジュアルなホーア論理とも呼ばれています。
ホーア論理は命令型プログラミングにおける形式論理の言語であるため、関数型(宣言型)プログラミングでは契約プログラミングとは異なるアプローチを取る方が適切だと考えています。例えば、副作用のない関数であれば、関数側ではなくデータ側に制約を持たせる方が自然に思えます。*6
おわりに
キーワード程度の提示しかできませんでしたが、使用するプログラミング言語が命令型だったとしても、宣言的な考えを用いることがバグを抑えることに繋がると考えています。