Haskellではモナドと呼ばれる部品を組み合わせてプログラムを作ります。状態を受け渡すタイプのモナドの使い方の初歩を説明します。ライブラリで用意されたモナドを手っ取り早く使うことを目的としているため、モナドの作り方や圏論には言及しません。
シリーズの記事です。
- Haskell 超入門
- Haskell 代数的データ型 超入門
- Haskell アクション 超入門
- Haskell ラムダ 超入門
- Haskell アクションとラムダ 超入門
- Haskell IOモナド 超入門
- Haskell リストモナド 超入門
- Haskell Maybeモナド 超入門
- Haskell 状態系モナド 超入門 ← この記事
- Haskell モナド変換子 超入門
- Haskell 例外処理 超入門
- Haskell 構文解析 超入門
- 【予定】Haskell 継続モナド 超入門
- 【予定】Haskell 型クラス 超入門
- 【予定】Haskell モナドとゆかいな仲間たち
- 【予定】Haskell Freeモナド 超入門
- 【予定】Haskell Operationalモナド 超入門
- 【予定】Haskell Effモナド 超入門
- 【予定】Haskell アロー 超入門
練習の解答例は別記事に掲載します。
この記事には関連記事があります。
他の言語に応用した記事があります。
状態系モナド
IOモナドは内部に隠された関数で状態を受け渡しています。他にも同様の構造を持つモナドがあり状態系モナドと呼ばれることがあります。
今回取り上げる状態系モナドは次の5つです。
※ 厳密な分類ではないため、人によって別の分類に含めるモナドもあります。
破壊的代入
変数の値を変更することを破壊的代入と呼びます。IORef
を使うことで破壊的代入を行うことができます。
import Data.IORef
main = do
a <- newIORef 1
writeIORef a 2 -- 破壊的代入
print =<< readIORef a
2
IORef
を使ってループでsum
を再実装してみます。
import Control.Monad
import Data.IORef
sum' xs = do
v <- newIORef 0 -- 初期値
forM_ xs $ \i ->
modifyIORef v (+ i) -- 更新
readIORef v
main = do
print =<< sum' [1..100]
5050
IOモナドは正規の方法では外せないため、IORef
を使うとIOモナドが付きまといます。これは破壊的代入が副作用として扱われるためです。
ここでのsum'
はローカル変数に対して破壊的代入を行っていますが、その影響は関数の外部には及びませんし、戻り値は引数だけで決まっています。破壊的代入を関数の中に閉じ込めれば、関数の外での参照透過性は保てるのではないでしょうか。
STモナド
STモナドは破壊的代入に特化したようなモナドです。STモナドを使えば破壊的代入を内部に閉じ込めて、計算結果だけを取り出すことができます。STは "state-transformer" の略です。
IOモナドの破壊的代入系の実装(IORef
やIOUArray
)に対応したSTモナド版の実装があります。
STRef
IORef
のSTモナド版です。
先ほどのIORef
を使った例をSTRef
に書き換えてみます。
import Control.Monad
import Control.Monad.ST
import Data.STRef -- ⇔ Data.IORef
sum' xs = runST $ do -- runSTでSTモナドから値を取り出す
v <- newSTRef 0 -- ⇔ newIORef
forM_ xs $ \i ->
modifySTRef v (+ i) -- ⇔ modifyIORef
readSTRef v -- ⇔ readIORef
main = do
print $ sum' [1..100] -- 戻り値がモナドに包まれていない
5050
IOモナドを使った例とほとんど同じですが、最後にrunST
でSTモナドを外しています。これができることがIOモナドとの決定的な違いです。
runST
runST :: (forall s. ST s a) -> a
STモナドから値を取り出す関数です。forall s.
はs
が具体的な型ではなく型変数のままであることを要求しています。詳細は後述します。
IOモナドでのunsafePerformIO
に相当します。unsafePerformIO
は使ってはいけないのに対して、runST
は普通に使える関数です。
import Control.Monad.ST
import Data.STRef
main = do
let a = do
b <- newSTRef 1
modifySTRef b (+1)
readSTRef b
print $ runST a -- STモナドを外す
2
型表記
ST s a
-
s
: 状態を表す型です。通常は型変数のまま、具体的な型は指定しません。 -
a
: STモナドの中に含まれる値の型です。
IOモナドと同じように状態を受け渡しますが、それが型にも表れています。ただし何か具体的な型を指定しなくても、型変数のままで使えます。
import Control.Monad.ST
main = do
let a = return 1 :: ST s Int -- sは型変数のまま
print $ runST a
1
forall
ST s a
のs
に具体的な型を指定してしまうとrunST
が使えなくなります。
import Control.Monad.ST
import Data.STRef
main = do
let a = return 1 :: ST Int Int -- ST s IntならOK
print $ runST a -- エラー
Couldn't match type `s' with `Int'
`s' is a rigid type variable bound by
a type expected by the context: ST s Int at src\Main.hs:6:13
Expected type: ST s Int
Actual type: ST Int Int
(略)
※ forall
は数学記号の∀に相当します。forall
を指定することを明示的な全称量化と呼びます。
IOモナドとの関係
STモナドのs
をRealWorld
に固定すればIOモナドと相互変換できます。
-
ST RealWorld a
⇔IO a
つまりSTモナドを特殊化したものがIOモナドだと見なせます。
内部関数
STモナドもIOモナドと同じように内部に値を生成するための関数を持っています。それぞれの型を示します。
-
ST s a
の内部関数:State# s -> (# State# s , a #)
-
IO a
の内部関数:State# RealWorld -> (# State# RealWorld, a #)
IOモナドがs
にRealWorld
を指定したSTモナドだということが分かります。
しかし型を明記していないだけで、実際はSTモナドもState# RealWorld
で動きます。
{-# LANGUAGE UnboxedTuples #-}
import GHC.Base
import GHC.ST
unST (ST f) = f -- STモナドからパターンマッチで内部関数を取り出す
main = IO $ \s ->
let f1 = unST $ return 1 -- STモナドから内部関数を取り出す
f2 = unIO $ print r1 -- IOモナドから内部関数を取り出す
(# s1, r1 #) = f1 s -- STモナドから取り出した内部関数を評価
(# s2, r2 #) = f2 s1 -- IOモナドから取り出した内部関数を評価
in (# s2, r2 #)
1
unIO
に相当するunST
が用意されていないため自前で実装しています。
realWorld#
realWorld# :: State# RealWorld
任意の場所で使えるState# RealWorld
型の変数です。runST
はIOモナドから状態を受け取っているわけではなく、realWorld#
を使っています。
これを使って先の例を書き換えます。
{-# LANGUAGE MagicHash, UnboxedTuples #-}
import GHC.Base
import GHC.ST
unST (ST f) = f
main = do
let f = unST $ return 1 -- STモナドから内部関数を取り出す
(# _, a #) = f realWorld# -- 内部関数をrealWorld#で評価
print a -- 内部関数レベルの操作は不要
1
main
をdo
で記述してprint
が普通に使えるようになりました。
stToIO
stToIO :: ST RealWorld a -> IO a
STモナドからIOモナドに変換する関数です。
import Control.Monad.ST
import Data.STRef
main = do
let a = return 1 :: ST s Int
print =<< stToIO a -- IOモナドに変換して値を取り出す
1
ioToST
ioToST :: IO a -> ST RealWorld a
逆向きの変換関数ioToST
もあります。
変換先はST RealWorld a
でs
に具体的な型が指定されておりrunST
は使えないため、STモナド経由でIOモナドを外すような抜け道は塞がれています。
STUArray
IOUArray
のSTモナド版です。
import Control.Monad
import Control.Monad.ST
import Data.Array.ST
main = do
let arr = runST $ do
a <- newArray (0, 5) 0 :: ST s (STUArray s Int Int)
forM_ [0..5] $ \i ->
writeArray a i i
getElems a
print arr
[0,1,2,3,4,5]
arr
の作成過程で破壊的代入を使用していますが、何事もなかったかのように普通のリストとして取得できています。
練習
【問1】次のJavaScriptによるBrainf*ckの前処理をSTモナドで書き直してください。
var bf = "+++++++++[>++++++++<-]>.";
var jmp = [];
for (var i = 0, loops = []; i < bf.length; ++i) {
jmp[i] = 0;
switch (bf[i]) {
case '[':
loops.push(i);
break;
case ']':
var start = loops.pop();
jmp[start] = i;
jmp[i] = start;
break;
}
}
console.log(jmp);
[ 0, 0, 0, 0, 0, 0, 0, 0, 0, 21, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 9, 0, 0 ]
⇒ 解答例
【問2】STモナドを扱うbind
とreturn'
を実装してください。
具体的には次のコードが動くようにしてください。
main = do
print $ runST $
return' 1 `bind` newSTRef `bind` \a ->
modifySTRef a (+1) `bind` \_ ->
readSTRef a `bind` return'
2
⇒ 解答例
Stateモナド
IOモナドやSTモナドは内部に特殊な関数を持っており、関数に状態を渡すと更新後の状態と値がアンボックス化タプルで返って来ます。しかし通常の使い方ではその仕組みが表に出てくることはありませんし、積極的に触るようなものでもありません。
それに対してこれから紹介するStateモナドは次のような特徴を持っています。
- モナド内部に「状態を受け取って、値と更新後の状態を返す」関数があります。
- 状態は通常の型で、戻り値も通常のタプルです。
- 内部関数や状態の存在が隠されていません。
このようにStateモナドはSTモナドと同じような構造ですが、特殊な型は使わずに実装されており、内部仕様がオープンになっています。
※ これは普通とは逆の導入です。Stateモナドを先に説明して、その発展形としてSTモナドやIOモナドを導入するのが普通です。しかし今まで使ったことがないもので説明されても位置付けが分かりにくいと判断したため、この超入門シリーズではStateモナドより先にIOモナドやSTモナドを説明しました。
Stateモナドを見ることで、IOモナドやSTモナドを平易な実装で復習してみましょう。
型表記
State s a
-
s
: 状態を表す型です。型変数のままでも、具体的な型でも構いません。 -
a
: Stateモナドの中に含まれる値の型です。
使用するにはmtlパッケージが必要です。
import Control.Monad.State
a :: State s Int
a = return 1
main = return ()
(出力なし)
STモナドとよく似ています。
runState
runState :: State s a -> s -> (a, s)
Stateモナドから値を取り出す関数です。
STモナドのrunST
に相当しますが、いくつか異なる点があります。
- 明示的に状態を渡す必要があります。
- 戻り値には状態が含まれます。
- タプル内の順番がSTモナドと逆です。(後述)
return
で入れた値を取り出すだけであれば、状態は()
でも構いません。
import Control.Monad.State
main = do
let a = return 1 :: State s Int
print $ runState a () -- ()は初期状態(必須)
(1,())
戻り値が普通のタプルで返されて、状態が含まれているのに注目してください。このように状態がオープンに扱われるのがSTモナドとの決定的な違いです。
タプル
StateモナドとSTモナドでは値と状態を扱うタプルの仕様が異なります。
- Stateモナド:
(値, 状態)
(通常のタプル) - STモナド:
(# 状態, 値 #)
(アンボックス化タプル)
タプル内の要素の順番が異なるのに注意が必要です。
片方だけを取得する関数
値だけ、状態だけ、を取得する関数もあります。
- 値だけ:
evalState :: State s a -> s -> a
- 状態だけ:
execState :: State s a -> s -> s
runState
から片方を捨てているだけなので、やはり明示的に状態を渡す必要があります。
import Control.Monad.State
main = do
let a = return 1 :: State s Int
print $ runState a () -- (値, 状態)
print $ evalState a () -- 値
print $ execState a () -- 状態
(1,())
1
()
内部関数
s -> (a, s)
runState
に状態を渡さずに部分適用したものが内部関数です。
※ 実際の内部関数はIdentityという別のモナドの中に入っています。詳細は続編のHaskell モナド変換子 超入門で説明します。
import Control.Monad.State
f :: () -> (Int, ()) -- 状態 → (値, 状態)
f = runState $ return 1 -- 部分適用により内部関数を取り出す
main = do
let (a, _) = f () -- evalState相当
print a
1
IOモナドとの比較
StateモナドとIOモナドは基本的な構造が同じで、Stateモナドは一般的な型だけを使って実装されているのを確認してください。
{-# LANGUAGE MagicHash, UnboxedTuples #-}
import GHC.Base
f :: State# RealWorld -> (# State# RealWorld, Int #)
f = unIO $ return 1
main = do
let (# _, a #) = f realWorld#
print a
1
state
state :: (s -> (a, s)) -> State s a
内部関数からStateモナドを作る関数です。
中に1
を含むStateモナドを自作してみます。
import Control.Monad.State
main = do
let m1 = return 1 -- 1が入ったモナド
m2 = state $ \s -> (1, s) -- m1と等価: 状態 -> (値, 状態)
print $ runState m1 ()
print $ runState m2 ()
(1,())
(1,())
タプルの順序が逆ですが、それ以外はIOモナドやSTモナドとほとんど同じです。
Stateアクション
Stateモナドが受け渡している状態を、1つの破壊的代入が可能な変数だと見なして使うことが可能です。
そのためにStateモナド専用のアクションが用意されています。これをIOアクションと区別してStateアクションと呼びます。
-
get :: State s s
: 状態を読み取ります。 -
put :: s -> State s ()
: 状態を書き換えます。 -
modify :: (s -> s) -> State s ()
: 関数で状態を更新します。
簡単な例を示します。
import Control.Monad.State
test = do -- 状態 -> (値, 状態)
a <- get -- runStateで渡された状態を取得
put $ a + 1 -- 先に取得した状態+1を新しい状態に設定
modify (* 2) -- 状態に2を掛ける
return a -- 最初の状態を値として返す
main = do
print $ runState test 5
(5,12)
空中から値を出し入れしているように見えますが、モナドがコンテキストを構成しています。
sum
modify
を使ってsum
を再実装してみます。ラムダ式を後に回すためセクションで引数の記述順をいじっています。
※ execState a b
⇔ a `execState` b
⇔ (`execState` b) a
import Control.Monad
import Control.Monad.State
sum' xs = (`execState` 0) $ do -- セクションで初期状態`0`を先に指定
forM_ xs $ \i ->
modify (+ i) -- 状態を更新
main = do
print $ sum' [1..100]
5050
先に示したSTRef
を使った実装よりシンプルですが、do
のコンテキストとして状態が受け渡されていることを認識していないと、理解するのは困難です。
状態は破壊的代入が可能な1個の変数だと見なせますが、変数が複数必要な場合はタプルを使うなどの工夫が必要になります。そこまでするよりも、実用上はSTRef
を使った方が簡単です。
アクションの定義
先頭の要素を取り出すアクションgetch
を定義して、それを使って3つの要素を取り出すアクションget3
を定義する例です。
import Control.Monad.State
getch = do -- 1文字取得するアクション
x:xs <- get -- 状態(リスト)を取得して分割
put xs -- 状態を更新
return x -- 先頭の要素を返す
get3 = do -- 3文字取得するアクション
x1 <- getch -- 1番目の要素を取得
x2 <- getch -- 2番目の要素を取得
x3 <- getch -- 3番目の要素を取得
return [x1, x2, x3] -- 取得した要素を連結
main = do
print $ evalState get3 "abcd"
"abc"
※ 文字が足りないとエラーになります。エラー処理は今回の範囲を超えるため、詳細は続編のHaskell モナド変換子 超入門やHaskell 例外処理 超入門で説明します。
モナドなしと比較
先ほどの例をStateモナドを使わずに定義します。
getch (x:xs) = (x, xs) -- 状態 -> (値, 状態)
get3 s0 = -- 最初の状態
let (x1, s1) = getch s0 -- 値と更新された状態
(x2, s2) = getch s1 -- 値と更新された状態
(x3, s3) = getch s2 -- 値と更新された状態
in ([x1, x2, x3], s3) -- (値, 状態)
main = do
print $ fst $ get3 "abcd"
"abc"
明示的に状態を受け渡しています。アクションを使えば状態の受け渡しが暗黙的に行われるのと比較してください。
内部関数によるモナド化
getch
をそのままStateモナドの中に入れてモナドを構築すれば、アクションを使って定義するよりも短くなります。get3
はその手法でモナド化するよりも、アクションを使って記述した方が直感的でしょう。
import Control.Monad.State
getch = state getch where -- 次の行で定義したgetchでモナドを構成
getch (x:xs) = (x, xs) -- モナドなしで定義したのと同じ関数
get3 = do -- 状態は自動処理されるので表に出ない
x1 <- getch
x2 <- getch
x3 <- getch
return [x1, x2, x3]
main = do
print $ evalState get3 "abcd"
"abc"
この例が分かりやすいかは判断の分かれるところですが、getch
のようなリーフ関数(アクション)は内部関数を直接記述した方が便利なこともあります。
練習
【問3】Stateモナドを扱うbind
, return'
, get'
, put'
を実装してください。
具体的には次のコードが動くようにしてください。
fib x = (`evalState` (0, 1)) $
(replicateM_ (x - 1) $
get' `bind` \(a, b) -> put' (b, a + b)) `bind` \_ ->
get' `bind` \v -> return' $ snd v
main = do
print $ fib 10
55
⇒ 解答例
【問4】問3のfib
をdo
と<-
で書き直してください。問3で再実装した関数は使わないでください。
⇒ 解答例
Readerモナド
Reader r a
読み取り専用のStateモナドに相当します。明示的に状態の変更を禁止したいときに使います。
型変数r
はReaderの頭文字ですが、状態を表します。
ask
ask :: Reader r r
状態を読み取るReaderアクションです。
Stateアクションのget
に相当します。アクションはモナドと紐付けられているため、get
はReaderモナドでは使用できません。
runReader
runReader :: Reader r a -> r -> a
StateモナドのrunState
に相当する関数です。Readerモナドは状態が変更されないため、値しか返しません。
両者を並べて比較します。
import Control.Monad.State
import Control.Monad.Reader
main = do
print $ (`runState` 1) $ do -- State
a <- get
return $ a + 1
print $ (`runReader` 1) $ do -- Reader
a <- ask -- getと同じ(モナドによる使い分け)
return $ a + 1
(2,1)
2
reader
reader :: (r -> a) -> Reader r a
関数からReaderモナドを作成します。Readerモナドでは状態は変化しないため、値だけを返す関数を渡します。
中に1
を含むReaderモナドを自作してみます。
import Control.Monad.Reader
main = do
let a = reader $ \_ -> 1 -- 状態は返さないため無視
print $ runReader a ()
1
local
local :: (r -> r) -> Reader r a -> Reader r a
別のReaderモナドに変更された状態を渡す関数です。元のReaderモナドのコンテキストには影響を与えません。Stateモナドのmodify
を限定的に行うのに相当します。
ネストで考えると分かりやすいです。
import Control.Monad.Reader
main = do
print $ (`runReader` 1) $ do
a <- ask -- 状態を確認
b <- local (+ 1) $ do -- ネスト
b' <- ask -- localによる(+ 1)が影響
return b'
c <- ask -- ネスト外では状態に変化はない
return (a, b, c)
(1,2,1)
-
a
: 状態として1が渡されます。 -
b
: ネストしたモナドから結果を受け取ります。-
local
で状態に1を足して、ネストしたモナドに渡します。 -
b'
: 状態を取得して、そのままreturn
で結果として渡します。
-
-
c
: 状態が変わっていないことを確認します。
練習
【問5】Readerモナドを扱うbind
, return'
, ask'
, local'
を実装してください。
具体的には次のコードが動くようにしてください。
test x = (`runReader` x) $
ask' `bind` \a ->
(local' (+ 1) $
ask' `bind` \b' ->
return' b') `bind` \b ->
ask' `bind` \c ->
return' (a, b, c)
main = print $ test 1
(1,2,1)
⇒ 解答例
【問6】問5のtest
をdo
と<-
で書き直してください。問5で再実装した関数は使わないでください。
⇒ 解答例
Writerモナド
Writer w a
追記専用のStateモナドに相当します。ログを取るのに便利です。
単なる書き込みではなく追記なのがポイントです。主にリストに追加して使います。
型変数w
はWriterの頭文字で、状態を表します。
※ w
にはMonoid
型クラス制約が掛かっていますが、今回の範囲を超えるため省略します。
tell
tell :: w -> Writer w ()
状態に追記するWriterアクションです。
Stateアクションのmodify
に近いです。アクションはモナドと紐付けられているため、put
やmodify
はWriterモナドでは使用できません。
runWriter
runWriter :: Writer w a -> (a, w)
StateモナドのrunState
に相当する関数です。初期状態は空だと決まっているため渡す必要はありません。
両者を並べて比較します。
import Control.Monad.State
import Control.Monad.Writer
main = do
print $ (`runState` "") $ do -- State
modify (++ "a") -- 追記
modify (++ "b") -- 追記
modify (++ "c") -- 追記
return ()
print $ runWriter $ do -- Writer
tell "a" -- 追記
tell "b" -- 追記
tell "c" -- 追記
return ()
((),"abc")
((),"abc")
状態だけを返すexecWriter :: Writer w a -> w
もあります。
階乗
もう少し実用的な例を示します。
階乗を計算する過程のログを取ります。1つの文字列に追記するのではなく、文字列のリストに追記しているのがポイントです。
import Control.Monad.Writer
fact 0 = do
tell ["fact 0 = 1"]
return 1
fact n | n > 0 = do
let dbg = "fact " ++ show n ++ " = " ++
show n ++ " * fact " ++ show (n - 1)
tell [dbg]
n' <- fact (n - 1)
let ret = n * n'
tell [dbg ++ " = " ++ show n ++ " * " ++ show n' ++ " = " ++ show ret]
return ret
main = do
let (a, w) = runWriter $ fact 5
putStr $ unlines w
print a
fact 5 = 5 * fact 4
fact 4 = 4 * fact 3
fact 3 = 3 * fact 2
fact 2 = 2 * fact 1
fact 1 = 1 * fact 0
fact 0 = 1
fact 1 = 1 * fact 0 = 1 * 1 = 1
fact 2 = 2 * fact 1 = 2 * 1 = 2
fact 3 = 3 * fact 2 = 3 * 2 = 6
fact 4 = 4 * fact 3 = 4 * 6 = 24
fact 5 = 5 * fact 4 = 5 * 24 = 120
120
比較としてIOモナドによるデバッグを再掲します。
fact 0 = do
putStrLn "fact 0 = 1"
return 1
fact n | n > 0 = do
let dbg = "fact " ++ show n ++ " = " ++
show n ++ " * fact " ++ show (n - 1)
putStrLn dbg
n' <- fact (n - 1)
let ret = n * n'
putStrLn $ dbg ++ " = " ++ show n ++ " * " ++ show n' ++ " = " ++ show ret
return ret
main = do
print =<< fact 5
実行結果は同じです。
writer
writer :: (a, w) -> Writer w a
タプルからWriterモナドを作成します。Writerモナドでは既存の状態を参照しないため、関数ではなくタプルとなっています。
中に1
を含むWriterモナドを自作してみます。
import Control.Monad.Writer
main = do
let a = writer (1, "") -- 状態は使わないので何でも良い
print $ runWriter a
(1,"")
※ 状態は文字列ではなくモノイドを対象とするため、この実装は本来のreturn 1
とは異なります。モノイドについては今回の範囲を超えるため詳細は省略します。
練習
【問7】Writerモナドを扱うbind
, return'
, tell'
を実装してください。
具体的には次のコードが動くようにしてください。
test = execWriter $
tell' "Hello" `bind` \_ ->
tell' ", " `bind` \_ ->
tell' "World" `bind` \_ ->
tell' "!!" `bind` \_ ->
return' ()
main = print test
"Hello, World!!"
⇒ 解答例
【問8】問7のtest
をdo
と<-
で書き直してください。問7で再実装した関数は使わないでください。
⇒ 解答例
関数モナド
関数もモナドとして扱えます。引数を状態として受け渡します。考え方はReaderモナドに似ています。
※ a -> b
は->
が関数モナドの型として定義されているため((->) a b
)、モナドとして扱えるようになっています。リストの[]
が型としても使える([] a
)のと似ています。今回の範囲を超えるため詳細は省略します。
例
関数をbindしたものも関数のため、do
ブロック全体で1つの関数が形成されます。そこに渡した引数が状態として受け渡され、含まれる関数に対して引数として渡されます。
test = do
a <- (+ 1) -- (+ 1) 5
b <- (* 2) -- (* 2) 5
return (a, b)
main = do
print $ test 5
(6,10)
練習
【問9】先ほどの例のtest
を通常の関数として書き直してください。
⇒ 解答例
まとめ
関数やアクションは主要なものを紹介しています。詳細はリンク先のリファレンスを参照してください。
- STモナド
-
Stateモナド(読み書き)
- 関数
-
runState
: 状態 → (値, 状態)-
evalState
: 状態 → 値 -
execState
: 状態 → 状態
-
-
state
: 関数s -> (a, s)
→ Stateモナド
-
- アクション
-
get
: 読み -
put
: 書き -
modify
: 更新
-
- 関数
-
Readerモナド(読み)
- 関数
-
runReader
: 状態 → 値 -
reader
: 関数r -> a
→ Readerモナド
-
- アクション
-
ask
: 読み -
local
: 別のReaderモナドに更新した状態を渡して評価
-
- 関数
-
Writerモナド(追記)
- 関数
-
runWriter
: → (値, 状態)-
execWriter
: → 状態
-
-
writer
: タプル(a, w)
→ Writerモナド
-
- アクション
-
tell
: 追記
-
- 関数
- 関数モナド
オブジェクト指向との比較
状態系モナドにおける状態は、モナド内部の関数を直接触らないでdo
ブロックを書いているだけだとコードには表面上現れません。そのため状態の受け渡しでコンテキストが構成されているという感覚は、初めのうちは分かりにくいです。
Javaなどのオブジェクト指向言語では、明示的に定義しなくてもthis
がインスタンスメソッドの中で使えます。インスタンスメンバへのアクセスにはthis
が省略できるため、暗黙的なコンテキストであるとも見なせます。
状態系モナドのdo
ブロックに流れるコンテキストは、オブジェクト指向言語でのthis
のようなものだと考えればイメージしやすいかもしれません。