Hot Heart, Cool Mind.

会計×IT の深層へ

サルでもわかるIOモナド①-副作用の除去

 このところ、仕事の手が空いたときに、Haskell というプログラム言語を勉強しています。Haskellは僕がふだん使っているJavaなどとはかなり趣きが異なるので、 考えさせられる点が多く、気分転換にはぴったりです。

 とはいえ、最初はさっぱり理解できない点もありました。「IOモナド」です。 Web上の色々な資料を読んでいるうちにだいたいのところは理解できたのですが、そうなってみると、今度は、 最初に読んだ何冊かの入門書やWeb記事でのIOモナドの説明の仕方が気になってきました。
こうした記事は、素人向けに易しくIOモナドを説明しようとして、その結果、かえって読者を煙に巻いてしまっている面があるんじゃないかと思います。

 IOモナドは、Haskell の鬼門とか呼ばれているようですが、わかってみるとさほど難しいものではありません。であれば、僕自身がきわめて素人くさくIOモナドを理解した仕方をご説明するのも、どなたかの役に立つかもしれません。

 というわけで、この記事です。


====

IOモナドの目的

 まずIOモナドの目的を確認しておきますね。
 IOモナドの目的は2つあります。ひとつめは、IO操作にともなう副作用をプログラムから除去することです。 ふたつめは、関数の評価順序を指定できない(しない)のが特長の関数型言語で、IO操作の実行順序を確実に指定できる手段を 提供することです。この2つの目的が Haskell でどのように達成されているかを順番に見ていきたいと思います。
今回は副作用の除去について検討します。

副作用の除去

 プログラム言語の本の最初に必ず出てくるプログラムを、Haskell で書いてみました:

	main = putStrLn "Hello, World!"

 このプログラムを実行すると、コンソールに、"Hello, World!" と表示されますね。いきなり副作用です。やられちゃいましたね。
ところで、この「副作用」は「いつ」起きているのでしょうか。その問いに答えるために、この式が何をしているか、 分析してみましょう。

 式の左辺(main)は変数です。右辺(putStrLn "Hello, World!")は式です。右辺の式はアクションという型の値を 返します。実はアクションはモナドでもあるわけですが、ひとまずそれは忘れて頂いて結構です。
 さて、ここが肝心です。putStrLn 自身がアクションなのではありません。文字列を受けてアクションを返す関数なのです。
 putStrLn "Hello, World!" という式は、評価されたときにアクションを返すだけで、自分自身がコンソールに "Hello, World!" と書き出すのではない、ということです。
 putStrLn に副作用がないというのはこういう意味です。

 じゃあ、書き出すのは誰でしょうか。実は Haskell の処理系です。
"main" という特別の変数をこのアクションに束縛することによって、アクションが処理系に引き渡されます。
処理系は渡されたアクションを実行します。このときに "Hello, World!" が書き出されるのです。つまり、副作用という「汚れ仕事」は、処理系が 引き受けているわけです。この例でのアクションは、「コンソールに"Hello, World!"と書き出せ」という指示書のようなものです。

 Haskell では、 main = で始まるプログラムはIO処理の指示書を作ることに専念し、その指示書に従って処理系がIO処理を実際に行うわけです。 だから、main 変数の型はアクションすなわちIO処理の指示書なのです。

 まとめると、Haskellプログラムの「実行」は、まったく異なる2つの仕事から成り立っているということです:

	Haskellプログラムの「実行」 = ①「main = ...」式の「評価」 + ②評価結果であるアクションの「実行

①はIO処理を伴いません。IO処理は②で起きるのです。だから、「main = ...」式は、副作用をまぬかれているのです。

 以上の話は筋が通っているが証拠がないと考える方のために、もうひとつ、例を挙げましょう:

	main = let a = putStrLn "Hello, World!" in (a >> a)

 このプログラムは、putStrLn "Hello, World!" が返すアクション a を 二回続けて実行するアクションを(>>)で生成して main 変数に設定しています。 Haskell の遅延評価方式では、putStrLn "Hello, World!"の「評価」は一回しか行われないはずです。一方、アクション a は二回「実行」されます。 はたしてこのプログラムを実行すると、"Hello, World!"が二回出力されます。

モナドは何のために?

 先に、モナドのことはひとまず忘れてくださいと書きました。 ここまで僕がしてきたように、モナドの概念を用いなくとも、Haskell がどのように副作用を除去しているかは説明できます。 実は副作用の除去とモナドの間に直接の関係はないのです。

 多くの解説書が、ここのところを曖昧にしているので、僕を含めた初心者が混乱するわけです。

 モナドとアクションの微妙な関係が出てくるのは、もうひとつの要件「IO操作の実行順序を確実に指定する」を満たすアクション を作ろうとした時なのです。そのことは次回ご説明します。

副作用と世界

 ここまでの話はご納得頂けたでしょうか。本やWebでIOモナドのことを学ばれた方は、違和感を感じられたかもしれません。 というのは、いくつかの本などでは、僕が説明したのとは違った仕方で副作用が除去されているような説明があるからです。
 具体的にいうと副作用を除去するために「世界」という概念を持ち出すのです。簡単にご説明しましょう。

 このアプローチでは、副作用のある関数 y=f(x) から副作用を除去するためには、 副作用によって影響を受ける「世界」(の状態)を、関数の定義に明示的に取り込めばよい、と考えます。 すなわち、上記の関数 f を変更して(y, world')= f(x, world) とすれば、副作用を除去できます。world とworld'は副作用を受ける前と後の世界の状態をそれぞれ表します。
 何が「主作用(?)」で何が「副作用」かは、その「作用」そのものの性質ではありません。 関数の定義に依存するのです。関数の引数と返り値の関係で説明が完結する作用は主作用。それ以外は副作用です。IO操作だからといって副作用だと決めつけるわけにはいかないのです。 そこを逆手にとって、副作用によって変化する「世界」そのものを引数と返り値に含めてしまえば、副作用なんてなくなってしまうわけです。
 「世界」を表現するデータ構造を実際にどう作ればうまくいくのかは僕の理解を超えています。 現実の世界の状態すべてを表現するわけにはいかないので、大胆な省略が必要なはずですが、どこまで省略して大丈夫なのか判断するのは、きっと難しい問題だろうと想像します。
 ただ、その点を除けば、これはまったく納得のいく話です。

 この話で注意しなければならないのは、このアプローチが使われる仕事は何かという点です。
 実はこれは、Haskell の処理系が担当する「アクションの実行」に関する話なのです。
 副作用という「汚れ仕事」は、処理系が引き受けていると、僕は書きました。このときは暗に、処理系は Haskell ではない言語で書かれているということを 前提としていました。しかし、処理系自体(あるいはその大部分)を Haskell で書くとしたらどうでしょう。 Haskell は副作用を引き受けられません。だから、ここで説明したように副作用を主作用に転化してしまうアプローチが必要になるわけです。
 具体的に言いましょう。上で示した関数(y, world')= f(x, world) において x を与えてやると(あるいは、束縛すると)、 (y,world') = g(world)という新しい関数が出来ます(このように関数の引数の一部を束縛することによって新しい関数を作ることを、カリー化というそうです)。 この関数 g を、アクションの実装とすることができます。 putStrLn を例にとりましょう。この関数のもとの形(カリー化前の形)は:

	putStrLnOriginal:: String -> World -> ( (), World )

と考えることができます。これをカリー化すると:

	putStrLnCurried:: String -> ( World -> ( (), World) )

となります。この関数の返り値の型が、上述した g と同じになっていることにご留意ください。これをアクションすなわちIO()に置き換えたのが:

	putStrLn:: String -> IO ()

見慣れた形ですね。

 これで、この「世界変換関数」の話が、処理系の実装方法に関するものであって、 Haskellプログラムから副作用を取り除く仕組みとは関係ないということにご納得頂けたでしょうか。ややっこしいですね。