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

state モナドと error モナドを一緒に使う

stackoverflow の 「エラー処理のできる state モナドはどう記述したらいいですか。」という質問の答が面白そうだったので実験してみた。返答されたプログラムを ghci で実験しやすいように少し変更したものが次のプログラム statemonad.hs だ。

newtype StateMonad e a = StateMonad {runStateMonad :: (State -> Either e (a, State))}

instance Monad (StateMonad e) where
  (StateMonad p) >>= k =
    StateMonad $ \s0 ->
      case p s0 of
        Right (val, s1) ->
          let (StateMonad q) = k val
           in q s1
        Left e -> Left e
  return a = StateMonad $ \s -> Right (a, s)

data State = State
  { log :: String
  , a  :: Int
  }
  deriving Show

実行例は次のようになる。

Prelude> :l statemonad.hs
[1 of 1] Compiling Main ( statemonad.hs, interpreted )
Ok, modules loaded: Main.
*Main> let state = State "hello" 1
*Main> let throwError e = StateMonad $ \s -> Left e
*Main> runStateMonad (return "success") state
Right ("success",State {log = "hello", a = 1})
*Main> runStateMonad (throwError "error") state
Left "error"

コードを追いかけてみた。

まず、モナドになるデータ型を作る必要がある。これは newtype キーワードで行う。newtype は制限付きの data 宣言と考えると分かりやすい。その制限とはコンストラクタは1種類で、コンストラクタのパラメータも1つだけだ。このプログラム例の場合はタイプ名が StateMonad e a でコンストラクタは StateMonad でコンストラクタのパラメータは ((State -> Either e (a, State)) だ。パラメータが無名関数になるのはモナドではよく使われるテクニックだ。

newtype StateMonad e a = StateMonad (State -> Either e (a, State))

プログラム例とは順序が前後するが、状態の State 型は次のようになる。

data State = State
  { log :: String
  , a  :: Int
  }
  deriving Show

プログラムの中心は MonadState を Monad クラスのインスタンスにして >>= と return の実装を行う部分だ。型名は StateMonad e a でモナド値のコンテナの値は a 型だから、 StateMonad e 型を Monad クラスのインスタンスにする。

instance Monad (StateMonad e) where

>>= の実装は次のようになる。順番に読んでいく。(StateMonad p) >>= k = が >>= 演算子の定義の部分だ。(StateMonad p) というパターンマッチで StateMonad 値のコンテナから無名関数 p を取り出す。

  (StateMonad p) >>= k =

StateMonad $ \s0 -> は s0 以下の無名関数を StateMonad コンストラクタでラッピングする事を示している。s0 の型は状態の State 型だ。

    StateMonad $ \s0 ->

case p s0 では >>= の左項の StateMonad 値から取り出した状態を引数とする無名関数 p を状態 s0 に関数適用している。その結果はエラーが発生しなければ Right (val, s1) が 、エラーが発生したときは Left e が返される。
      case p s0 of

Right (val, s1) が返ってきたときは、val に関数 k を関数適用した結果を (StateMonad q) のパターンマッチからコンテナの関数 q を取り出し、新しい状態 s1 に関数適用させる。

        Right (val, s1) ->
          let (StateMonad q) = k val
           in q s1

Left e が返ってきたときは、単に Left e を返す。

        Left e -> Left e

最後は return 関数の定義だが、 \s -> Right (a, s) を StateMonad にラッピングして返すだけだ。

  return a = StateMonad $ \s -> Right (a, s)

このプログラムでは Error モナドも State モナドも import されていない。Monad クラスのインスタンスを作って >>= と return を実装するだけで、State モナドと Error モナドの複合モナドの機能がプログラムできている。

この簡単なプログラム例を調べると、モナド・トランスフォーマーを使った複合モナドの作り方がブラックボックスではなくなるかもしれない。

自前のモナドを作るといっても要するにコンストラクタを定義して、そのデータ型に対して >>= と return の定義をするだけなのだ。なんか拍子抜けするが、意外に簡単なしくみだ。
by tnomura9 | 2013-08-27 21:55 | Haskell | Comments(0)
<< state モナドと err... 著者の文脈と読者の文脈 >>