Hot Heart, Cool Mind.

会計×IT の深層へ

サルでもわかるIOモナド②-逐次実行とモナドの登場

 前回は、「アクション」のご説明をしました。 Haskellのプログラムは、自分自身がIO操作をするかわりに、IO操作の指示書である「アクション」を返し、 処理系にIO操作という「汚れ仕事」を押し付けているのでしたね。だからHaskellプログラムは「副作用」という罪に染まらず、 ピュアでいられるのでした(考えようによっては偽善者ですね)。

 しかし、その説明の中ではモナドがほとんど登場しませんでしたね。 僕は、「モナドとアクションの微妙な関係が出てくるのは、IO操作の実行順序を確実に指定できるアクションを作ろうとした時なのです」 とか、苦し紛れの言い訳じみたことを言って、皆さんの気を持たせただけでした。

 さてこの道は本当に、モナドに通じているのでしょうか。

 それでは、続きをどうぞ。


====

並んで、並んで

 前回挙げた Hello World は単純なプログラムです。IO操作がひとつしかありません。一方、世界は複雑です。 偏屈なプログラマなら、何か他人にはうかがいしれない理由によって、"Hello," と "World!"をそれぞれ別のIO操作で書き出したい と考えるかもしれません。どうすればいいでしょうか。

 難しく考えてはいけません。「Haskell プログラムはIO処理の指示書を作る。あとは処理系にお任せ」というお気楽な原則にこだわるのです。
たとえばこんなの、どうでしょう:

	main = [putStr "Hello, " , putStrLn "World!" ]

 これは、コンパイルを通りません。Haskell のお約束では、main はアクション型なのに、この式の右辺はアクションのリストだからです。 しかし、そのお約束を脇に置くとすれば、何かまずいことがあるでしょうか。
 このアクションリストを受け取って、2つのアクションが指示しているIO操作を順に実行するような処理系を作ることができるはずです。

 このケースのようにアクションを逐次実行するというのが要件のすべてなら、アクションのリストを返すというソリューションで十分だったでしょう。

 でも世界はさらに複雑です。くだんのプログラマは、キーボードから入力された一文字を読み取って、それをコンソールに表示する、という悪夢のように複雑なプログラムを書きたくなりました。 「一文字を読み取る」というアクションをまず実行しなければ、その次の「一文字を表示する」というアクションを組み立てることすらできません(何を表示すればよいのかわからないじゃないですか)。 上述した「アクションリスト」ソリューションは、最初のアクションを実行する前に、実行すべきアクションを漏れなく用意しておくことができる、という前提に立っています。 ここで、その前提が、はかなく崩れてしまいました。

アクションリストの例からわかること

 うまくいかなかったとはいえ、アクションリストの例からわかることがあります。 この例ではアクションの実行順序を制御するためにリストを使いました。しかし、リスト自身が「逐次実行制御機構」みたいなものを 持っているわけではないし、持つ必要もないのです。リストの役割はただひとつ。処理系にアクションの実行順序を伝えることです。 リストには、要素が順序を持つという特徴がありますから、その順序がそのままアクションの実行順序を表すという約束(コントラクト) を処理系との間で取り交わしておけば、その役割を担えるのです。この点は、また後で話題にしますから、ちょっと気に留めておいて下さい。

アクションリスト改善版

 さて気を取り直して、僕らが置かれている状況をもう少し詳しく分析してみましょう。こんなところかな: 

  1. アクションが実行された結果として何らかの値が返されるケースがある。
  2. 返された値をもとにしなければ、次に実行すべきアクションを生成できないケースがある。
  3. アクションは、Haskellの通常の式の評価の結果として生成することができる。

 以上をもとに考えると、前の案のようにアクションを単純に並べたリストの代わりに、 一番目にはアクションを置き、二番目以降は、「直前のアクションの実行の結果得られた値をもとに次に実行すべきアクションを返す関数」を 並べたリストを作ればよいんじゃないでしょうか。アクションの実行結果として得られる値の型を a,b,c.... とすると、 こんな感じです。

	x::Action a,  f::a->Action b, g::b->Action c, h::c->Action d ....... 

たとえば、一文字読み取って一文字表示するプログラムなら:

  • アクションリストの先頭には「一文字読み取る」アクションを置きます。
  • アクションリストの二番目には、「一文字を受け取って、『その一文字を表示するアクション』を返す」関数を置きます。

処理が三段階以上になっても、同じように、アクションの実行結果を次の関数に引き渡していけばよいのです。 二段目以降を「アクションを返す関数」としたことによって、アクションとアクションの間でデータを引き渡すことが可能になりました。

 さて、この改善版アクションリストを処理系が実行する場面を想像してみましょう。処理系は最初のアクションを取り出し、それを実行します。 その結果得られた値をもとに、次段の関数が評価されます。その結果はアクションです。これをまた処理系が実行します。このプロセスを繰り返すだけです。  アクションの実行は処理系に依存し副作用を生みます。関数の評価は Haskell の文法通りに行われ、副作用を生みません。 前回挙げた単純な例では式の評価がすっかり終わってからアクションが実行されたのに対して、ここでは、それが交互に行われるという違いはあります。 しかし、処理系とHaskellプログラムの役割分担は同じです。

 なお、この改善版があれば、もとの素朴なアクションリストによるソリューションは必要ありません。 二段目以降に、引数の値を用いずにアクションを生成する関数を置けば、単純にアクションを逐次実行せよという指示書になるからです。

 あとは、このアクションと関数の並びを Haskell であらわす方法を考えればOKですね。残念ながら、リストは不適格です。 Haskellのリストは、要素の型がすべて同じでないといけませんが、この並びの要素はそれぞれ型が異なっていますから。

モナド登場

 実はモナドは、このような「アクションと関数の並び」を表現する仕組みなのです。ご説明しましょう。
 上の例では「アクションと関数の並び」を(リストとして)一気に作ろうとして型チェックに引っ掛かりました。 ですから、それをいったんあきらめて、先頭の アクション x と次の関数 f だけを結合する手段を考えます。この演算を (>>=)とします:

	x >>= f 

 (>>=) は左辺のアクション x と、右辺の関数 f を組み合わせて、新しい「何か」を作ります。何を作れば良いでしょうか。 僕たちはは先頭のアクションと次の関数 f をとりあえず結合しました。しかし、後にはもっと沢山の関数 g, h ...が残されています。 これらの関数も結合するためには、(>>=)で作られるのはアクションだということにしておけばよい。そうすれば、 (>>=)演算を次々に適用することによって、「アクションと関数の並び」の末尾に関数をいくつでも継ぎたしていくことができます:

	(x >>= f) >>= g 
	((x >>= f) >>= g) >>= h 

 なお、(>>=)は左のものほど優先されると決めておけば、括弧をはずせます:

	x >>= f >>= g 
	x >>= f >>= g >>= h 

 「合成されたアクション x >>= f を実行する」とは「①アクション x を実行し、②その結果得られた値 a をもとに 式 f a を評価し、 ③その評価結果であるアクションを実行する」ことである、という約束(コントラクト)を処理系と交わしておけば、Haskellプログラムは指示書作りに専念できます。

 式 x >>= f において、x の型が Action a なら、f の型は a->Action b でなければなりません( ただし b は任意の型です )。 このとき、合成アクションが返す結果は b 型になりますから、合成アクションの型は、Action b です。

 ねっ。これはモナドの(>>=)そのものでしょ?

 僕たちは、IO操作の副作用を除去し、実行順序を保障する方法を探求する旅の終りに「アクションと関数の並び」という概念に辿り着きました。 その概念をうまく表現しようと工夫した結果、アクションがモナドなっちゃったのです。 アクションをモナドしたから、副作用が除去され、実行順序が保障されたのではないのです。 ここのところで原因と結果を取り違えると、IOモナドはよくわからんということになってしまいます。

 最初に挙げた素朴なアクションリストの場合と同様、(>>=)演算や合成アクションの中に処理を逐次実行する仕組みが隠されている必要もありません(隠されていても構わないのですが、それは処理系の実装に係る選択肢のひとつに過ぎないということです)。 合成アクションの責任は、アクションひとつと関数ひとつの「対(つい)}を表現することだけです。 その責任さえまっとうして呉れれば、その対を、実行と評価の順序を明確に示した指示書として、処理系に渡すことができます。これで十分なのです。

次回へ続く

 さてこれで、モナドとアクションの微妙な関係をお伝えすることができたでしょうか。
 でも、やり残したことがまだあります。
 まず、モナドにはいくつか要件があります。アクションがこれらの要件を実際に満たしえることを確認する必要があります。 次に、モナドの直観的意味について、アクションの例をもとに考えてみたいと思います。それに、前回ふれた「世界変換関数」についても、 逐次実行機構の実装手段という視点から改めて眺めてみると楽しそうです。しかし、今日はもうおなかいっぱい。
また、次回のお楽しみ、ということでお許しください。