初めに
この記事は、Haskell初心者がStateTモナドに試行錯誤する記事です
生暖かい目で見守っていただければ幸いです。
方針
出発点である状態付き計算から、1つ1つ段階を踏んで考えていきたいと思います。
例題
Lisp構文チェッカを設定します。
構文チェッカといっても括弧の位置、向き、数だけで構文木の中身は一切触れません。
これを例に段階を踏んで進めていきます。
#1 再帰による引き回し
'(' を +1 、 ')' を -1 と設定し、値が常に0以上かつ最後に0であれば括弧は正しい、という形で実装します。
checkParent :: Int -> String -> Bool
checkParent s "" =
if (s == 0)
then True
else False
checkParent s (x:xs) =
if (s >= 0)
then
case x of
'(' -> checkParent (s + 1) xs
')' -> checkParent (s - 1) xs
_ -> checkParent s xs
else False
checkParent 0 "(aaa (bbb ccc))"
-- => True
このように、Haskellにおいてただ状態を持ちたい、というだけなら一応はモナドなしでも可能です。
しかし、1つでも引数が増えると、引き回しではとても手に負えなくなってしまいます。
そこで出てくるのがStateモナドです。
#2 Stateモナド
その1
type StateData = (String, Int)
checkParentS :: State StateData Bool
checkParentS = do
(str, s) <- get
if str == ""
then
if s == 0
then return True
else return False
else do
let (x:xs) = str
case x of
'(' -> put (xs, s+1)
')' -> put (xs, s-1)
_ -> put (xs, s)
checkParentS
runState checkParentS ("(aaa (bbb ccc))", 0)
-- => (True,("",0))
引き回しver. から直訳しました。
ただ、これだけでは違いがわかりにくいので、文字数のカウントも加えてみます。
その2
type StateData2 = (String, Int, Int)
checkParentS2 :: State StateData2 Bool
checkParentS2 = do
(str, s, count) <- get
if str == ""
then
if s == 0
then return True
else return False
else do
let (x:xs) = str
case x of
'(' -> put (xs, s+1, count+1)
')' -> put (xs, s-1, count+1)
_ -> put (xs, s, count+1)
checkParentS2
runState checkParentS2 ("(aaa (bbb ccc))", 0, 0)
-- => (True,("",0,15))
引き回しと違って、引数を増やすこともなく簡単にパラメータを増やせました。
さらに複雑になったとしても、StateDataのデータ構造を変更するだけで、簡単に対応できるでしょう。
#3 IO処理を加える
構文チェッカなので、ログの書き出し機能を加えます。
checkParentS3 :: State StateData2 IO Bool
checkParentS3 = do
-- 以下省略
‘State’ is applied to too many type arguments
In the type signature for ‘checkParentS3’:
checkParentS3 :: State StateData2 IO Bool
どうやらStateモナド内でIOモナドは扱えないようです。
ここで出てくるのがStateTモナドです。
#4 StateTモナド
checkParentST :: StateT StateData2 IO Bool
checkParentST = do
(str, s, count) <- get
if str == ""
then
do
lift $ writeFile "log.txt" $ (show count) ++ " characters"
if s == 0
then return True
else return False
else do
let (x:xs) = str
case x of
'(' -> put (xs, s+1, count+1)
')' -> put (xs, s-1, count+1)
_ -> put (xs, s, count+1)
checkParentST
runStateT checkParentST ("(aaa (bbb ccc))", 0, 0)
-- => (True,("",0,15))
'T'を加えてlift関数を用いるだけで、IO処理をStateモナド内に簡単に書けました。
結論
StateTモナドとは、Stateモナド内において、IOを行える、則ちモナドを扱えるようにしたものである。
最後に
Hakell初心者なので、ここが間違っているとか、何かあったらコメントしていただけると幸いです。