「モナド会」とは、モナドをまともに使ったことがない人間が、モナドどころか関数型プログラミングの経験もない人間に、モナドについて解説するという恐ろしい会である。
実は以前、モナドについての記事を書いたことがある。
モナドについての知識が全くない頃に(今でもかなり怪しいが)、Philip Wadler 氏の論文を読んで、少し分かった気になったので軽い気持ちで書いた記事だ。しかしその後、何の間違いなのか、「モナド」でググるとこの記事が1ページ目に表示されるようになってしまった。本当に申し訳ない気持ちでいっぱいである。
この責任ある立場としては、「モナド会」なるものを開催し、分かったつもりの勢いで初心者に解説を試みて、そしてその成果をここで紹介することでより混乱を深めていくしかない、そのように決意した次第である。
というわけで、今回は「モナド会」で説明を試みた話題の中から、最も根本的な話である「そもそもなんで関数型プログラミングが必要なのか?」というお題について紹介してみたい。
オブジェクト指向と不確定性
関数型プログラミングのメリットは、これまでの主流を占めていたオブジェクト指向プログラミングとの比較で考えると分かりやすい。
一言で言えば、オブジェクト指向と比較して関数型は「原因と結果を局所化するので、システムの動きが分かりやすくなる」。
どういうことだろうか?
以下の図を見て欲しい。
オブジェクト指向では、システムに何か動きがあったとき、その動きの原因となる箇所と、結果となる箇所が分散しているため、システムの動作(状態遷移)が把握しづらくなる。上図で言うと、青い部分が原因になる箇所で、赤い部分が結果として状態変更の行われる可能性のある箇所だ。
まず、青丸に Arg
と書かれている method
の引数が動作の入力になるというのは、比較的すんなりと理解できる。ところが、図をよく見ると、Devices
と書かれた箱も青い線で囲まれていて、入力の一部になっていることが分かる。Devices
は、プログラムの外部にあるサービスを表している。単にオブジェクトを利用するときには意識に上らないことが多いが、実は Devices
の状態も事前条件として、動作に影響を与える「原因」の一部になっている。
さらに、結果の方を見てみると、動作に関係しているオブジェクトそれぞれの状態が変更される可能性がある上に、Devices
にも状態変更が起こる可能性があることが分かる。
Devices
を操作するのがプログラムのそもそもの目的なのだから、当たり前と言えば当たり前の事態なのだが、オブジェクト指向言語でユニットテストを書いたことがある人なら、なんとなくこれらの厄介さが分かるのではないだろうか。
とあるメソッドのテストを書く場合、単純に引数を渡して実行すればOKというわけには行かず、依存オブジェクトやシステムについて、何らかの準備が必要になることが多い。そして、結果を検証する際にも、オブジェクトの境界だけを確認するか(Mockist Testing)、あるいは分散したシステムの状態を確認するか(Classical Testing)といった選択に悩むことになる。
このように、プログラムから直接接続された Devices
(外部サービス)は、プログラムの挙動を予測しづらくする諸悪の根源なのである。
純粋関数 – 原因と結果の局所化
関数型では、この原因と結果を、関数の入出力として局所化するため、システムの動きが格段に分かりやすくなる。
この原則が徹底されているとき、つまり、システムで発生し得る状態遷移の全てが関数の入出力として表現されてるとき、これらの関数を純粋な関数と呼ぶ。
純粋な関数だけで構築されたシステムでは、その入出力として表現されている以外の出来事は起こらない。つまり、関数の入出力を見ればシステムがどのように動くかを完全に把握できるということだ。
そのようなシステムを図にしたのが以下である。
システムが動くときの原因と結果が、関数の入出力として局所化されていることが分かる。
これでシステムの状態遷移がシンプルになったし、めでたしめでたしと言いたいところだが、オブジェクト指向で Devices
にアクセスしてた部分はどうなったのだろうか? Devices
を操作できなければ、まともなプログラムは作れないはずである。
実はそこに純粋関数型プログラミングのトリックがある。
図をよく見ると、関数は入力を得て出力を返すのみで、Devices
へ直接アクセスすることはないものの、入出力を受け渡しするレイヤーとして Runtime
というものが Devices
との間に挟まっている。
純粋関数型は、Devices
を直接操作できない代わりに、Devices
への命令をデータとして出力し(図中の出力に Command
が含まれていることに注目)、それを Runtime
に渡すことによって、間接的に Devices
を操作する。
このようなややこしい迂回をすることによって、外部サービスをプログラムから切り離して、関数の純粋性を保つわけである。
Devices
への命令をデータ化して、プログラム全体を純粋関数にしようとするのは、関数型プログラミングの中でも最もハードコアな部類になるとは言え、基本的に関数型プログラミングは、純粋関数を出来るだけ多く導入することによってシステムから不確定性を取り除こうとする考え方だと言って良いのではないだろうか。
純粋関数にはメリットがある。しかし、それを徹底しようとすると「命令をデータとして表現する」というややこしい方法を取らざるを得ない。その結果、命令型の言語のような簡潔さは失われることになる。その失われた簡潔さを取り戻すために「モナド」のような仕組みが活躍することになるのだが、これはまた続きの記事で紹介したいと思う。