Haskell でバグの出にくいプログラミング (1)

ソフトウェア開発で最も時間のかかる作業は、デバッグであると言われています。 よって、バグをいかに出にくくするかで開発効率を改善することができると考えられます。


バグの温床、副作用

バグは主に副作用 (コンピュータの状態の変化) のある所に好んで生息します。 副作用のある処理はその実行する順序が重要となり、処理を記述する順番をきちんと決める必要があります。 そのため、ロジックの組み間違いからバグが発生する可能性があります。 また、予期せぬ場所で予期せぬ値の変更が起こり、それがバグの原因になることもあります。

事実、ロジックの組み違いを減らすために、ソフトウェアの設計をし、個々のコンポーネントの機能を明確、簡潔にし、予期せぬ値の変更を減らすためにカプセル化、といった手法が取り入れられ、今日でも使われています。


Haskell で副作用を分離

バグを出にくくするための機構を持ったプログラミング言語は数多くありますが、その中でもよりバグが出にくい潜在的な能力を持つ言語に、Haskell があります。 Haskell は、副作用の起こる部分と起きない部分とを明確に分離することができるのです (これは、極度なカプセル化ともいえるかもしれません。) しかし、このバグが出にくいという利点を前面に押し出している Haskell に関する記事はあまり見かけません。 なので、これから何回かに分けて、Haskell での副作用の分離とそれによって得られる利点について説明していきます。


IO 制御

副作用の一つには、IO 制御があります。 デバイスとの入出力は基本的にコンピュータの論理的な状態を変化させるために行われるからです。 Haskell では、IO 制御を行う関数の返り値の型は、IO a (a は任意の型) と決められています。 IO a 型の返り値を持つ関数のみが、IO 制御を行うことができ、それ以外の関数は基本的に IO 制御をすることはできません。 例えば、g を IO 制御を行って文字列を返す関数とします。

g :: IO String   -- 型の定義
ここで別の関数 f が g で得られた結果を出力する関数であるとすると、
f = g >>= putStrLn   -- 関数の定義
この時、>>= は、その左側の IO 制御の結果を右側の関数に渡す関数で、putStrLn は、引数として与えられた文字列を改行付きで標準出力に出力する関数です。 これらの関数の型の定義は、以下のような感じです。
(>>=) :: IO a -> (a -> IO b) -> IO b    -- a, b は任意の型
putStrLn :: String -> IO ()
>>= の型の定義は厳密に言うとやや違うのですが、簡単のためこうしています。 また、putStrLn の IO () の () は、C などで言う void です。

それはさておき、このプログラムの f の型はなんでしょうか? g と >>= と putStrLn の型の定義から推測してみると、f の型は、IO () 型です。 f は、g と putStrLn という IO 制御を行う関数を使っているので、当然 f も IO 制御をする、ということになります。 さらに、f は g から受け取った文字列を標準入出力に出力しているだけなので、返り値はなく、結果として IO () という型を持ちます。

このように、副作用である IO 制御を行った関数は全て IO a 型を持ちます。 そのため、プログラマは関数の型を見るだけで、その関数内で IO 制御が行われるかを知ることができます。 また、IO 制御をしてははならない関数から、IO 制御をする関数を誤って呼ぶことも、コンパイラによる型のチェックによって防ぐことができます。


担当:齋藤 (説明文を書かせるとやたら長くなるのは仕様です)