人気ブログランキング | 話題のタグを見る

StateTモナドの内と外

複合モナドである StateT モナドを利用する目的は、その中で IO モナドの関数を利用するためだと思われている。実際 lift 関数を利用することで、 StateT モナドの中で IO モナドの関数 putStrLn "hello" は lift $ putStrLn "hello" を StateT モナド関数として使うことができる。

Prelude> import Control.Monad.State
Prelude Control.Monad.State> foo = do {lift $ putStrLn "hello"} :: StateT String IO ()
Prelude Control.Monad.State> runStateT foo ""
hello
((),"")

したがって、次のように StateT モナドの関数である get とも混在して使うことができる。

Prelude Control.Monad.State> bar = do {x <- get; lift $ putStrLn x} :: StateT String IO ()
Prelude Control.Monad.State> runStateT bar "hello"
hello
((),"hello")

この意味で IO モナドは StateT モナドの中で使うことができると言える。ところが、runStateT bar "hello" の戻り値は IO ((),"hello") である。したがって、runStateT 関数は IO モナド関数である。すなわち、runStateT 関数は IO モナドの中の関数ということになり、今度は State T モナドが IO モナドの中で使われているということになる。内と外が逆になってしまう。

したがって、今度は runStateT 関数が他の IO モナド関数と一緒に IO モナドの中で使うことができるということになる。

Prelude Control.Monad.State> do {runStateT foo "hello"; putStrLn "world"}
hello
world

StateT モナドの中に IO モナド関数が入っていたと思っていたのに runStateT 関数は IO モナド関数だったというモナド同士の内と外の関係は混乱する。しかし、lift $ putStrLn 関数は StateT モナドの中の関数であるが。そのままでは StateT モナドのプログラムは実行されない。それは、runStateT 関数の引数にすることによって初めてプログラムとして実行される。ところが、このプログラムの実行を担う runStateT 関数は StateT モナドのプログラムを実行後 IO a 型の IO モナド値を返すので IO モナド関数なのである。

StateT モナドと IO モナドの関数の内と外の関係が交錯してわかりづらいが、それぞれの関数が使われる場所に依存しているのだと考えるしかない。図にすると次のようになる。

do {runStateT (do {x <- get; lift $ putStrLn x}) "hello"; putStrLn "world"}

だた、上の入れ子構造で見られる、StateT モナドと IO モナドのそれぞれの関数の立ち位置の違いが分かるようになれば、複合モナドの利用を納得して行うことができる。慣れるしかない。

StateT モナドのプログラム例として、簡単なポーランド記法のパーサを書いてみた。ポーランド記法の計算は、演算子が先頭に来て関数のような働きをする。たとえば、+ + 1 2 3 は、+ (+ 1 2) 3 の意味で (1+2) + 3 という計算になる。途中経過を数値スタックに積まずに再帰下降型のパースで計算することができ、カッコを使わないのが特徴だ。下のプログラムは、StateT モナド内で evaluate の戻り値を表示するようにしているので、再帰的計算の過程が分かる。(文頭のアンダースコアはスペースで置換する。)

元のプログラムは過去記事「IOモナドでスタックを利用する」に掲載している。同じところをぐるぐる回っているが螺旋状に少しずつ上昇していると信じたい。

実行例
*Main Control.Monad.State> main
1
2
2
3
6

ソースプログラム

import Control.Monad.State

pop :: StateT [String] IO String
pop = do
__x <- get
__put (tail x)
__return (head x)

evaluate :: StateT [String] IO String
evaluate = do
____x <- pop
____case x of
______"*" -> do
________val1 <- evaluate
________lift $ putStrLn val1
________val2 <- evaluate
________lift $ putStrLn val2
________return $ show $ (read val1 :: Int) * (read val2 :: Int)
____ _ -> return x

main = do
__putStrLn =<< evalStateT evaluate ["*","*","1","2","3"]

by tnomura9 | 2019-05-27 02:51 | Haskell
<< いろいろなモナドを ghci ... State モナドでスタックを作る >>