IOモナドとHaskell の最も単純なイメージ

抽象的な記号を扱うときに大切なのは、それについての何か具体的なイメージを持っていることだ。

例えば集合のイメージなら丸い輪のなかに色とりどりのおはじきが入っているところを想像するし、関数のイメージは、二つの集合の要素のおはじきを糸で結んだ物が、おはじきの数だけあるところをイメージすると分かりやすい。

このイメージモデルを使えば、写像 f : A -> B が全射なら、集合Bのどのおはじきをつまみあげても、その端に集合Aのおはじきが付いてくるし、単射なら、集合Aのおはじきを二つ引っ張ると、必ずBの違うおはじきが引っ張られる。

全単射なら、集合Bのどのおはじきにも集合Aのおはじきはつながれており、集合Bの違うおはじきに対しては集合Aの同じおはじきが付いてくることはない。もしそうでなければ、集合Bの二つのおはじきにひとつの集合Aのおはじきがつながっていることになり、写像の条件を満たさないし、集合Bひとつに集合Aの二つのおはじきがつながっていたとしても、単射の条件を満たさないからだ。したがって、どの集合Bのおはじきにもひとつだけ集合Aのおはじきが付いてくるので、AとBのおはじきは必ず一対一につながっており、集合Aと集合Bの要素の数は等しいことが分かる。また、この状態では集合Bから集合Aへの f の逆写像 g を考えることができ、g ( f (x) ) = x であることがイメージ的に分かる。

IOモナドもそれに類するイメージがあるとずいぶん分かりやすいだろう。

ところで、>>= (bind演算子)の型を調べていて面白いことに気がついた。Hugs の type コマンドで >>= の型を調べると次のようになる。

Hugs> :type (>>=)
(>>=) :: Monad a => a b -> (b -> a c) -> a c

一般に、>>= は2つのIOモナドの関数を合成する演算子としてとらえられているだろう。例えば、

getLine >>= putStrLn

は、getLine とputStrLn という2つの関数を合成していると解釈することができる。

しかし、上の型の定義をよく見ると別の解釈もできる。つまり、 a b -> (b -> a c) -> a c はIOモナド型のデータ、IO b を関数 (b -> a c) に注入すると、IO c というIOモナド型のデータが出てくるとも解釈できる。(Haskell では矢印は単に引数の順序を表しているだけなので、プロックダイアグラムのような信号の流れを表しているわけではないが、この場合は偶然にそういう見方をしても意味が変わらない。)

この解釈なら、IOモナド型のデータを関数という機械に入れると、処理されて別のIOモナド型のデータがが出てくるというイメージになる。このイメージモデルなら、関数という機械に入れられるのはIO型のデータで、出てくるのもやはりIO型のデータだから、データを次々に数珠つなぎに関数に入れてつないでいくこともイメージしやすいし、関数という機械に入れるデータも出てくるデータもかならずIOモナド型のデータであるという閉じた世界であることもイメージできる。

実際、getLine >>= putStrLn の場合も、getLine の戻り値である IO String 型のデータを putStrLn 関数に注ぎ込んだと考える事ができる。この場合、データの注ぎ口にあたるのが >>= 演算子だとかんがえると具体的なイメージがわきやすい。そうしてその結果 IO () というやはりIOモナド型の値が出力される。

return 関数はデータをIOモナド型に包み込むだけの関数だから、getLine の代わりに使ってみるとこのイメージがよく分かる。

Hugs> return "Hi" >>= putStrLn
Hi

IO "Hi" が putStrLn 関数に入力されているのが分かる。ここで注意しなくてはならないのは、Hi と表示されたのは、putStrLn 関数の副作用だ、ここには現れていないが、putStrLn 関数が返すのは IO () というIOモナド型のデータだ。関数がIO型のデータを返す以外に、表示を出力したりするのが、IOモナドの関数がアクションと呼ばれる所以だ。

こういう風にイメージすると、IOモナドのシンプルなイメージを作ることができる。すなわち、IOモナドの世界は、IOモナド型のデータと、IOモナド型のデータを注ぎ込むとIOモナド型のデータを吐き出す、関数という機械からできており、IOモナドの世界のプログラミングは、この関数機械を数珠つなぎに組み合わせて工場のラインを作ることだというイメージだ。

ところが、演算子 $ の型について検討してみると、Haskell そのものについても同じアナロジーが適用できるという事が分かる。演算子 $ は関数を合成する演算子だがその型は次のようになる。

Hugs> :type ($)
($) :: (a -> b) -> a -> b

これは一見 >>= 演算子とはまったく別物のように見えるが、それは、IOモナドの処理の流れと、Haskell の関数の合成の処理の流れが表記上逆になるためで、上の定義を次のように変えると、

a -> (a -> b) -> b

>>= 演算子の場合と全く同じ解釈ができる事が分かる。つまり、関数にデータが注ぎ込まれ、処理されたデータが戻り値として関数から排出されるというイメージである。この場合も $ 演算子は関数へのデータの注ぎ口を示している。

このことは、2というデータを(*2)という関数に次のようにして注ぎ込むことができるのからも確認できる。

Hugs> (*2) $ 2
4

上に述べたことから、IOモナドやHaskell の世界についての、シンプルなイメージを作ることができる。つまりIOモナドという圏の世界も、Haskell という圏の世界も、その圏の世界にだけ流通するデータとその圏のデータを加工して、同じ圏の別のデータに変える関数という機械からなっており、その機械は次々につなげて工場のラインを作ることができるというイメージだ。そうして、その際に、>>= 演算子や、$ 演算子は、関数機械へのデータの注入口を指し示している。

こういうシンプルで映像的なイメージは、IOモナドのプログラミングを考える上でずいぶん役立ってくれるだろう。


今日のHaskell
Hugs> break (>5) [1..10]
([1,2,3,4,5],[6,7,8,9,10])

break は述語が真になる要素の前でリストを分割し、2つ組のタプルとして返す。
[PR]
by tnomura9 | 2010-11-03 18:44 | Haskell | Comments(0)
<< IOモナドのループ処理 Try Haskell >>