モナドを重ねる

えー、今日はモナドトランスフォーマーの一種StateTについて勉強してみました。参考にしたのはここら辺です。

モナドは実際に行われることの一つの面についての上手に抽象化してくれるのですが、一つ一つのことが別々のモナドで実現されているので、ステートも持ちたいけどIOも一緒に使いたいというときはどうしてもStateとIOの両方を持つもなどを使いたくなるわけで、どうやらそうゆうときに使うのがモナドトランスフォーマーということらしいです。StateTというのは別のモナドを型パラメタ(?)にとるモナドトランスフォーマーで、次のような定義になっています。

newtype StateT s m a = StateT { runStateT :: (s -> m (a,s)) }

instance (Monad m) => Monad (StateT s m) where
    return ::  a -> StateT s m a
    return a          = StateT $ \s -> return (a,s)
    (>>=) :: StateT s m a -> (a -> StateT s m a) -> StateT s m a
    (StateT x) >>= f  = StateT $ \s -> do
                                           (v,s')      <- x s
                                          (StateT x') <- return $ f v
                                          x' s'

気になるのはIOとStateの重ね方なわけですが、IO (State s a)なのかState s (IO a)なのか...でも、StateTは見つかってもIOTはどこにも見当たりません。実際無理やりIO (State s a)とやったら一体何が起きるんでしょうね…先述のリンク先を読んでいると、モナドの重ね方の判断は少しトリッキーなところがあるようです…

今回はStateTのドキュメンテーションにもStateT s IO aの記述があったので、StateとIOを重ねるのはそれが正解ということにして、先に進みました。IO(State s a)だと>>=::IOを越えてステート情報が渡ってくれないような気はしますね...という意味で応用性は低いのでしょうが、mainからStateモナドな関数を呼んでいる図はありえる構図なので、IO(State s a)が動かないというわけではないとは思います。
逆に、StateT s IO aだと、ステート情報を暗黙のうちに流しながらそこここでIOアクションが行えるので初歩的なゲームなんかには向いているということになるのでしょう…

今回は、先日のStateモナドの例に無理やりIOを突っ込んで、StateT s IO aを使ったサンプルを書いてみました。

module Main
	where

import Char
import Control.Monad.State

getInputNum :: IO Int
getInputNum = do
	putStrLn "Type a number"
	input <- getLine
	if (not.null $ filter isDigit input) then
		return $ digitToInt $ head $ filter (isDigit) input
		else fail "Quit"
		

compute :: Int -> StateT Int IO Int
compute a = do
	x <- get
	put (x + 1)
	i <- liftIO (getInputNum) 
	return (i * (a + x))

testM :: IO()
testM =(putStrLn.show) =<< evalStateT (mapM compute [1..5]) 1

testWithCatch = catch testM catcher
	where
		catcher :: IOError -> IO ()
		catcher e
			| e == (userError "Quit") = 
				putStrLn "Quitting..."
			| otherwise = ioError e

main = testWithCatch

このようにStateとIOが重なった状態でもmapMはしっかりと仕事をしてくれます。かっこいいですねー。

今回は、おまけで、IOモナドのもう一つの機能であるところの例外処理も入れてみました。getInputNumがstdinから数字以外の文字を受け取ると例外を発生して、mapMの途中でも飛び出してきてくれます。これもゲームにはおあつらえ向きですね…

computeMで使っているliftIOというのはMonadIOクラスのメソッドで、

class Monad m => MonadIO m where
Methods
liftIO :: IO a -> m a

ということになっていて、StateT s IO aな関数の中からIO aな関数を呼ぶのに使っています。面白いのは
i <- liftIO (getInputNum)
がさりげなく動いちゃうところですね…
liftIOの結果はStateT Int IO Intなわけで、それはつまり
 StateT \s -> (s, getInputNum)
というわけで

i <- liftIO (getInputNum)
  ||
StateT \s -> (s, getInputNum) >>= \i -> …

であって、StateTのbindはvalueを次に伝えてくれますから、iにはちゃんとgetInputNumの結果が入るというわけですね…

とてもきれいに収まっているところがすごいですねー。これがモナド則の威力ということなんでしょうか...モナドを3重4重に重ねても同じことがおきるんでしょうね...StateTのbindの定義内でdo文、つまりbindが使われていることから、何重にモナドを重ねても、ちゃんと動くということなんでしょう...恐ろしすぎます。

ではでは