Help us understand the problem. What is going on with this article?

Haskell 状態系モナド 超入門

Haskellではモナドと呼ばれる部品を組み合わせてプログラムを作ります。状態を受け渡すタイプのモナドの使い方の初歩を説明します。ライブラリで用意されたモナドを手っ取り早く使うことを目的としているため、モナドの作り方や圏論には言及しません。

シリーズの記事です。

  1. Haskell 超入門
  2. Haskell 代数的データ型 超入門
  3. Haskell アクション 超入門
  4. Haskell ラムダ 超入門
  5. Haskell アクションとラムダ 超入門
  6. Haskell IOモナド 超入門
  7. Haskell リストモナド 超入門
  8. Haskell Maybeモナド 超入門
  9. Haskell 状態系モナド 超入門 ← この記事
  10. Haskell モナド変換子 超入門
  11. Haskell 例外処理 超入門
  12. Haskell 構文解析 超入門
  13. 【予定】Haskell 継続モナド 超入門
  14. 【予定】Haskell 型クラス 超入門
  15. 【予定】Haskell モナドとゆかいな仲間たち
  16. 【予定】Haskell Freeモナド 超入門
  17. 【予定】Haskell Operationalモナド 超入門
  18. 【予定】Haskell Effモナド 超入門
  19. 【予定】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モナドの破壊的代入系の実装(IORefIOUArray)に対応した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 asに具体的な型を指定してしまうとrunSTが使えなくなります。

NG
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モナドのsRealWorldに固定すればIOモナドと相互変換できます。

  • ST RealWorld aIO a

つまりSTモナドを特殊化したものがIOモナドだと見なせます。

内部関数

STモナドもIOモナドと同じように内部に値を生成するための関数を持っています。それぞれの型を示します。
* ST s aの内部関数: State# s -> (# State# s , a #)
* IO aの内部関数: State# RealWorld -> (# State# RealWorld, a #)

IOモナドがsRealWorldを指定した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

maindoで記述して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 asに具体的な型が指定されており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モナドで書き直してください。

JavaScript
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モナドを扱うbindreturn'を実装してください。

具体的には次のコードが動くようにしてください。

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 ba `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のfibdo<-で書き直してください。問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のtestdo<-で書き直してください。問5で再実装した関数は使わないでください。

解答例

Writerモナド

型表記
Writer w a

追記専用のStateモナドに相当します。ログを取るのに便利です。

単なる書き込みではなく追記なのがポイントです。主にリストに追加して使います。

型変数wはWriterの頭文字で、状態を表します。

wにはMonoid型クラス制約が掛かっていますが、今回の範囲を超えるため省略します。

tell

tell :: w -> Writer w ()

状態に追記するWriterアクションです。

Stateアクションのmodifyに近いです。アクションはモナドと紐付けられているため、putmodifyは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モナドによるデバッグを再掲します。

Haskell アクション 超入門より

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のtestdo<-で書き直してください。問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モナド
    • 関数
      • runST: → 値
      • stToIO: STモナド → IOモナド
      • ioToST: IOモナド → STモナド
    • アクション
      • STRef: 破壊的代入が可能な変数
      • STUArray: 破壊的代入が可能な配列
  • 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のようなものだと考えればイメージしやすいかもしれません。

7shi
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした