モナドと実行順序のこと、結末
kazu-yamamotoさんの http://d.hatena.ne.jp/kazu-yamamoto/20080212/1202793403 に導かれてnobsunさんの
http://haskell.g.hatena.ne.jp/nobsun/20060907/monadicIO
http://haskell.g.hatena.ne.jp/nobsun/20060908
の2つの記事をざっと読みました。nobsunさんの丁寧な説明がすごいですね。IOモナドはすべてWorldをパラメータにとる「アクション」だというところ、なるほどそうだったんだと思いました。
色々と読んみてわかったのは、Stateモナドのように、IOも関数をパッケージ化しているモナドだと考えることができるということで:
IO a = World -> MakeIO a World
という風に見ることができる。だとすると、PutChar は以下のように解釈できる。
PutChar :: Char -> IO ()
||
PutChar :: Char -> World -> MakeIO () World
だから、nobsunさんの言われるところのIO*を評価しても入出力は実行されないというのは、つまり
PutChar 'a' :: IO ()
||
PutChar 'a' :: World -> MakeIO () Wolrd
ということで、PutChar 'a' の評価結果はWorldをとってMakeIO () Worldを返す関数であって、誰かがWorldを渡してくれなければPutChar自身は実行されないということのようです。そして、HaskellではWolrdをパラメータにとる関数を評価することを「実行」と呼ぶといってもよいのではないかなと思いました。
でもHaskellのどこを見てもWorldの定義は見つかりません。そしてIOモナドはOneWayモナドなので、一度IOモナド型になってしまったものからIOをとることはできません。(Control.Monad.Trans.liftIOはIO a -> m a型でIOモナドをほかのモナドに紛れ込ませることができるようですが、これは緊急用ということなのでしょうか?細かい説明は見つけられませんでした。追記:liftIOについては誤解が解けましたのでこちらにて報告しています。)
さらにIOモナドのを「実行」するためには何とかしてWorldを持ってこなくてはならない。でもWorldの定義はHaskellのコードからは参照できない。たった一つmainだけは IO()型なので、nobsunさんの言われるように、mainにはWorldがわたってきているということで、残りすべてのIO モナド型の関数はmainからIOモナドの鎖でつながった形になる。−>つまり実行順序が決定される、ということになるらしいです。
さらにわかったことは、IOモナドのbindやreturnの中では別段特殊なことはやっていなくて、Worldパラメターの依存連鎖を使って実行順序が決定されるようです。
先日のポストで誤った考察をしてしまった理由は以下のものです。リストとIOのbindの動きの違いを見つけようとして以下のようなコードを書いてみました。
module Main where import Control.Monad import Data.Char chars :: [Char] chars = repeat 'a' charsIO :: IO String charsIO = return $ repeat 'a' main :: IO() main = do (return $ take 5 $ chars >>= return.toUpper) >>= putStrLn (return $ chars >>= return.succ) >>= putStrLn5 (liftM (\x -> x >>= return.succ.succ) charsIO) >>= putStrLn5 where putStrLn5 = putStrLn.(take 5)
メインの中で実行されている3つの行は上から
1.リストのbindで無限文字列をtoUpperに渡して、その結果の先頭5文字を表示する。
2.リストのbindで無限文字列をsuccに渡して、その結果をIO型でくるみ、IOのbindでputStrln5に渡す。
3.IO String型の無限文字列をbind::IOでputStrLn5に渡す。
僕は>>=::IO(IO型のbindと読んでください)に左辺の処理を待つコードが入っていると思い込んでいたので、2,3については何も表示せずにハングすると思っていました。ところが、実際に実行してみると、2も3も5文字だけ表示して、ちゃんと終了します。
立ち返って先日のコードと上のコードを比較すると、
module Main where ch :: IO Char ch = return 'a' chars :: IO String chars = sequence $ repeat ch main :: IO() main = chars >>= ( (mapM_ (putChar)).(take 5))
違いはcharsがsequenceを使っていることぐらいです。そこで、charsを
chars = return $ repeat 'a'
に変えると、やはりこのコードもハングしなくなりました。これはつまり、>>=::IOは特にIOのために特殊な処理を行っているわけではないということを示していたわけですね…ついでに言うと、sequenceがハングした理由は内部でfoldrを使っているからのようです。foldrはリストの終端からfoldしていきますので、無限リストではハングするしかないということのようです。(涙)
>>=::IOが実行順序を強制していないとなると、いったい何が実行順序を強制するのか…nobsunさんの記事を読んでわかったのは僕が上で例示したIOモナドを使う関数はすべてWorldパラメタを変化させないということです。なので、賢きHaskellコンパイラは僕が例の中で書いたどのIOアクションも実際にWorldに何の作用も起こさないことを見抜いて最適化してしまったのだと思います。では誰がWorldを変化させる力を持っているかというと、それはHaskellのランタイム内で実際にOSなどの副作用を起こす関数を呼び出すccallなどの内部ルーチンだけのようです。
これでだいたい終わりかなと思ったのですが、最後に引っかかるのがkaz_yamamotoさんがつぶやいていた「">>=" も ">>" も、糊なのかな?」というのを読んでいて僕が勝手に心配したことで、IOモナドの構文で>>を使うとWorldの連鎖が切れちゃったりすることがあるのか、って、そんことはありえないですよね...ということで、>>=::IOと>>について考えてみました。
>>=::IOは大体以下の感じで実装されていると予想されます。
(>>=) :: IO a -> (a -> IO b) -> IO b (>>=) :: (World -> MakeIO a World) -> (a -> World -> MakeIO b World) -> (World -> MakeIO b World) (>>=) ma f = \w -> MakeIO b world'' where MakeIO a world' = ma w MakeIO b world'' = f a world'
一方>>はPreludeで定義されていて:
(>>) :: m a -> m b -> m b
(>>) m k = m >>= \_ -> k
となっています。だから、
(>>) :: IO a -> IO b -> IO b (>>) :: (World -> MakeIO a World) -> (World -> MakeIO a World) -> (World -> MakeIO a World) (>>) ma mb = \w -> MakeIO b world'' where MakeIO a world' = ma w MakeIO b world'' = (\_ -> mb world') a
となってWorldの連鎖はちゃんと確保されているという感じでしょうか…>>=::IOがプラットフォーム依存な理由はそうしないとWorldやMakeIOをHaskellのコードから隠蔽できないからということでしょうか...
ということでIOモナドと実行順序に関するお勉強、ひとまずおしまいです。