Anatomy of a monad transformer
このセクションでは、標準ライブラリのモナドの中でも興味深いモナドの transformer バージョンの実装について詳しく調べる。それは StateT monad transformer だ。StateT モナド・トランスフォーマーを調べれば、monad transform のしくみを知ることができるので、その他の monad transformer を利用するときのコツを掴むことができる。ただし、このセクションを読むにあたって State モナドに慣れていない場合は、もう一度 State モナドのセクションを読み返しておいたほうがいいかもしれない。 Combined monad definition State モナドは次のように定義されている。 newtype State s a = State { runState :: (s -> (a,s)) } 注: 上の定義から、State モナドのモナド値のコンテナの runState フィールドには s -> (a,s) 型の関数が収められていることがわかる。この関数を呼び出すためには runState アクセサを使わなければならない。ghci で試してみると次のようになる。 Prelude> import Control.Monad.State Prelude Control.Monad.State> runState (return 3) 5 (3,5) return 3 で s -> (3,s) という関数が State データコンストラクタにラッピングされて State モナド値が返される。そのモナド値 (return 3) に runState データアクセサを関数適用すると、(return 3) の runState フィールドの値である s -> (3,s) が取り出され、引数の 5 に適用される。その結果最終的な値 (3,5) を得ることになる。(3,5) の 3 は値で、5 が状態だ。 このあたりのしくみは標準モナドに共通なので把握しておかなければならない。要点は State モナドのモナド値のコンテナには値ではなく、関数が収められていて、その関数は runState データアクセサで取り出すことができるということだ。 注終わり StateT monad transformer は上の State モナドの定義を利用して次のように定義されている。 newtype StateT s m a = StateT { runStateT :: (s -> m (a,s)) } 注: StateT monad transformer のモナド値のコンテナには s -> m (a,s) が収められている。State モナドのときは s -> (a,s) だったので、違いは戻値が m (a,s) のようにペアを m モナドでラッピングしたモナド値であるということだ。State モナドの場合は関数の戻値は単にペアだ。ghci で戻値の型をみてみるとその差異がわかる。 Prelude Control.Monad.State> :t runStateT (return 3) 5 runStateT (return 3) 5 :: (Monad m, Num s, Num a) => m (a, s) Prelude Control.Monad.State> :t runState (return 3) 5 runState (return 3) 5 :: (Num s, Num a) => (a, s) しかし、runStateT (return 3) 5 を ghci 上で実験しても runState (return 3) 5 の時と表示が変わらない。 Prelude Control.Monad.State> runStateT (return 3) 5 (3,5) それは、ghci のコマンドラインのプログラムは IO モナドとして実行されるので、(3,5) と IO (3,5) を区別して表示してくれないからだ。しかし、runStateT (return 3) 5 の戻値は IO モナド値なので >>= 演算子で print と結合することができる。 Prelude Control.Monad.State> (runStateT (return 3) 5) >>= print (3,5) しかし、runState (return 3) 5 の戻値は単なるペアなので >>= で print と結合しようとするとエラーになる。 Prelude Control.Monad.State> (runState (return 3) 5) >>= print Couldn't match expected type `(a0, b0)' with actual type `IO ()' Expected type: a1 -> (a0, b0) Actual type: a1 -> IO () In the second argument of `(>>=)', namely `print' In the expression: (runState (return 3) 5) >>= print 注終わり State s モナドは Monad クラスと MonadState クラスのインスタンスだ。したがって、StateT s m モナドも Monad クラスと MonadState クラスのインスタンスでなければならない。また StateT データコンストラクタの m パラメータのモナドが MonadPlus クラスのインスタンスであれば、StateT s m モナドも MonadPlus クラスのインスタンスである必要がある。 StateT s m モナドを Monad クラスのインスタンスに定義する方法は次のようになる。 newtype StateT s m a = StateT { runStateT :: (s -> m (a,s)) } instance (Monad m) => Monad (StateT s m) where return a = StateT $ \s -> return (a,s) (StateT x) >>= f = StateT $ \s -> do (v,s') <- x s -- get new value, state (StateT x') <- return $ f v -- apply bound function to get new state transformation fn x' s' -- apply the state transformation fn to the new state State s モナドの定義と比べてみよう。StateT s m モナドの定義では return 関数を内側のモナド inner monad の return 関数を用いて定義している。また、bind 演算子を定義するのに inner monad の do ブロックを用いている。 注: return 関数の構造は単純なのでこれをまず解読してみる。 return a = StateT $ \s -> return (s, a) 上の定義の StateT $ ... から return 関数の役割は \s -> return (s,a) という無名関数を作って StateT データコンストラクタでラッピングして StateT s m a 型のモナド値を戻値として返すことだと分かる。 \s -> return (s,a) のうち a は return の引数だし、s は無名関数のラムダ抽象された変数だから、問題はない。しかし、\s -> return (s,a) の return の意味はなんだろうか。retrun は多相関数なので、対応するモナドによって意味が変わってくる。 だだし、この場合は StateT データコンストラクタのパラメータの関数の型は、StateT s a m のタイプコンストラクタで規定されるので、return は m モナドの関数であることは明白だ。したがって \s -> return (s, a) は \s -> m (s, a) と同じになる。 次に bind 演算子 >>= について見てみよう。bind 演算子の定義は次のようになっている。 (StateT x) >>= f = StateT $ \s -> do (v, s') <- x s (StateT x') <- return $ f v x' s' (StateT x) は StateT モナド値からパラメータの x :: s -> m (a, s) 型の関数を取り出すためのパターンだ。f は StateT モナドの Kleilsi 圏の射で f :: \x -> State s m a 型の関数でなければならない。= StateT $ ... だから、do 以下のプログラムの値は最終的に StateT データコンストラクタでラッピングされることになる。 したがって、StateT $ の後の do は文脈からそれ以下のプログラムが m モナドで記述されることを意味している。なぜなら、x :: s -> m (a, s) 型の関数だから、x s の値は m モナドのモナド値 m (a, s) だからだ。 したがって、(v, s') <- x s は状態 s に関数 x :: s -> m (a, s) を関数適用して得られる m モナドのモナド値 m (a, s) のコンテナから (a, s) を取り出し、v = a、s' = s に束縛することを意味している。 (SateT x') <- return $ f v は m モナドの値 v を f に関数適用させて得られる部分関数 f v :: s -> m (a, s) をパターンマッチで x' に束縛することを意味している。 最後に x' に新しい状態 s' を関数適用すると新しい a' と s' を含んだ m (a', s') 型の値になる。do の前には StateT $ \s -> があるので最終的な >>= の値は StateT { runState = \s -> m (a' s') } となる。 StateT 複合モナドの return 関数の定義は s -> m (a, s) 型の関数を作って StateT データコンストラクタでラッピングしているだけだ。StateT の return の定義をするのに、inner monad の return 関数を使って記述するのは、inner monad の型がどのようなモナドでも同じ定義が適用されるためだ。これによって inner monad のタイプによって StateT の return 関数を書き換える必要がなくなる。bind 演算子の定義に do 記法を使うのも同じ理由からだ。 注終わり StateT モナドは MonadState クラスのインスタンスにしないといけないので、get と put の実装をする必要がある。 instance (Monad m) => MonadState s (StateT s m) where get = StateT $ \s -> return (s,s) put s = StateT $ \_ -> return ((),s) 最後に StateT タイプコンストラクタのパラメータ m のモナドが MonadPlus クラスのインスタンスである場合、StateT s m モナドも MonadPlus のインスタンスにする必要がある。したがって、mzero と mplus の実装をしなければならない。 Defining the lifting function 最後に、StateT s m モナドをフル装備のモナドにするためには、これを MonadTrans クラスのインスタンスにして、lift 関数を実装しなくてはならない。 instance MonadTrans (StateT s) where lift c = StateT $ \s -> c >>= (\x -> return (x,s)) lift 関数は monad transformer の内側のモナドの関数を、外側のモナドの関数に変換する。原則としてモナドの do 記法の中には一種類のモナドしか置けないが、lift 関数で内側のモナドを外側の複合モナドに変換することができる。 次の関数は StateT s IO モナドの例だ。do 構文の中には StateT モナドしか置けないが、lift 関数の引数にすることによって IO モナドの関数 print x を StateT モナドに変更して使っている。 Prelude> import Control.Monad.State Prelude Control.Monad.State> runStateT (do x <- get; lift (print x); return (x*2)) 5 5 (10,5) All About Monad の本文には、State モナドとリストモナドの複合モナドである StateT s [] モナドを例にあげて説明しているが、リストモナドの動作はなれないと直観的に理解するのが難しいのでスキップする。 注終わり Functors このセクションでは monad transformer の1つを取り上げて説明してきた。上でも述べたように標準モナドの transformer バージョンを作るのに定型的な方法はない。それぞれの monad transformer のバージョンは、動作の内容を考慮し個別に設計しなければならない。 それにもかかわらず、monad transformer についての基礎的な理論はある。あるタイプの monad transformer は内側のモナドをどのように取り扱うかという点で、ひとまとめにすることができる。そうして、各々の複合モナドについては、複合モナドをモナドの関数 monadic function と関手 functor を使って導き出す事ができる。Functor とはおおまかに言えば、fmap :: (a -> b) -> f a -> f b を提供するクラスのことだ。さらに学ぼうと思ったら、Mark Jones のよく知られた論文がある。Haskell の標準モナドのライブラリはこの論文に基づいている。 前へ 目次 次へ
by tnomura9
| 2013-08-03 16:48
| Haskell
|
Comments(0)
|
カテゴリ
新型コロナウイルス 主インデックス Haskell 記事リスト 圏論記事リスト 考えるということのリスト 考えるということ ラッセルのパラドックス Haskell Prelude Ocaml ボーカロイド 圏論 jQuery デモ HTML Python ツールボックス XAMPP Ruby ubuntu WordPress 脳の話 話のネタ リンク 幸福論 キリスト教 心の話 メモ 電子カルテ Dojo JavaScript C# NetWalker ed と sed HTML Raspberry Pi C 言語 命題論理 以前の記事
最新のトラックバック
最新のコメント
ファン
記事ランキング
ブログジャンル
画像一覧
|
ファン申請 |
||