モナド変換子を使ったトレース
では,いよいよモナド変換子を使ったトレースの仕方を紹介しましょう。
printはIOモナドの中で処理を行う関数であるのに対し,モナド変換子tを適用した結果のt IOモナドはIOとは別のモナドです。したがって,モナドmに対する関数をモナドt mに対して適用するための手段が必要です。このために用意されているのが,Control.Monad.TransモジュールのMonadTransクラスで提供されているliftメソッドです。
class MonadTrans t where lift :: Monad m => m a -> t m a
liftメソッドは,fmapやControl.MonadモジュールのliftMが関数をモナドに対して適用可能にするのと同様に,モナドmに対する関数をモナドt mに対して適用可能なように持ち上げます。これがliftという名前の意味です。
instance MonadTrans (StateT s) where lift m = StateT $ \s -> do a <- m return (a, s)
Prelude Control.Monad.State Control.Monad.Identity> runIdentity $ runStateT (put 33.1 >> get >>= return) 22 (33.1,33.1) Prelude Control.Monad.State Control.Monad.Identity> runIdentity $ runStateT (fmap (+1) (put 33.1 >> get >>= return)) 22 (34.1,33.1) Prelude Control.Monad.State Control.Monad.Identity> :i liftM liftM :: (Monad m) => (a1 -> r) -> m a1 -> m r -- Defined in Control.Monad Prelude Control.Monad.State Control.Monad.Identity> runIdentity $ runStateT (liftM (+1) (put 33.1 >> get >>= return)) 22 (34.1,33.1) Prelude Control.Monad.State Control.Monad.Identity> runStateT (put 33.1 >> get >>= \x -> lift (print x) >> return x) 22 33.1 (33.1,33.1) Prelude Control.Monad.State Control.Monad.Identity> :t runStateT (put 33.1 >> get >>= \x -> lift (print x) >> return x) 22 runStateT (put 33.1 >> get >>= \x -> lift (print x) >> return x) 22 :: (MonadState s (StateT s IO), Fractional s) => IO (s, s)
liftメソッドを使った計算もそうでない計算も,一つのモナドmの中で行われるため,Debug.Traceモジュールとは異なり,遅延評価に悩まされることはありません。つまり「評価されないかもしれないから出力されないかもしれない」という問題を回避できます。また,モナドなので,トレースの挿入がプログラム全体の構造を乱すこともありません。モナドをトレースする場合には,Debug.Traceよりもモナド変換子を使うほうがよいでしょう。
なおliftメソッドの定義は,モナド変換子則(monad transformer law)と呼ばれる以下の二つの法則を満たすように定義される必要があります。
- lift . return == return
- lift (m >>= k) == lift m >>= (lift . k)
モナド変換子同士の組み合わせ
モナド変換子は単体でも便利ですが,より威力を発揮するのが,複数のモナド変換子を組み合わせてより大きいモナド変換子を組み立てる場合です。状態,非決定性計算,継続といった様々な計算を行う複数のモナド変換子を組み合わせていくことで,多くの能力を持つ一つの巨大なモナドを組み立てることができます。この方法には,モナド変換子による計算の階層が深くなればなるほど全体の計算速度が低下するという欠点がありますが,それほど階層を重ねない場合やプロトタイプを作る場合にはあまり問題にはならないでしょう(参考リンク)。
モナド変換子同士を組み合わせるのが有効なのは,様々な計算を行う大きなモナドを作りたいときだけではありません。StateT s [] aのようなモナドに対してトレースを行いたい場合にも,モナド変換子同士を組み合わせられると便利です。実際にやってみましょう。
Control.Monad.Listモジュールに,Listモナドをモナド変換子にしたListTが定義されています。
newtype ListT m a = ListT { runListT :: m [a] } instance (Monad m) => Functor (ListT m) where fmap f m = ListT $ do a <- runListT m return (map f a) instance (Monad m) => Monad (ListT m) where return a = ListT $ return [a] m >>= k = ListT $ do a <- runListT m b <- mapM (runListT . k) a return (concat b) fail _ = ListT $ return [] instance (Monad m) => MonadPlus (ListT m) where mzero = ListT $ return [] m `mplus` n = ListT $ do a <- runListT m b <- runListT n return (a ++ b) instance MonadTrans ListT where lift m = ListT $ do a <- m return [a]
糖衣構文の恩恵を受けられないため少し複雑な書き方になっていますが,ListTは期待通り,リストと同じように振る舞います。
Prelude Control.Monad.List> do {x <- [3,1,4]; guard (x > 2); [x, x+1]} [3,4,4,5] Prelude Control.Monad.List> do {x <- return 3 `mplus` [1,4]; guard (x > 2); [x,x+1]} [3,4,4,5] Prelude Control.Monad.List> runIdentity $ runListT $ do{x <- return 3 `mplus` return 1 `mplus` return 4; guard (x > 2); return x `mplus` (return $ x+1)} [3,4,4,5]
次に,StateTとListTを組み合わせたモナド変換子を作成してみましょう。
Prelude Control.Monad.List Control.Monad.State> runListT $ runStateT (do{ x <- return 3 `mplus` return 1 `mplus` return 4; guard (x > 2); return x `mplus` (return $ x+1)}) 12 [(3,12),(4,12),(4,12),(5,12)] Prelude Control.Monad.List Control.Monad.State> runListT $ runStateT (do{ x <- return 3 `mplus` return 1 `mplus` return 4; guard (x > 2); lift (put 22); return x `mplus` (return $ x+1)}) 12 <interactive>:1:96: Ambiguous type variables `m', `t' in the constraint: `MonadState t m' arising from a use of `put' at <interactive>:1:96-101 Probable fix: add a type signature that fixes these type variable(s) <interactive>:1:100: Ambiguous type variable `t' in the constraint: `Num t' arising from the literal `22' at <interactive>:1:100-101 Probable fix: add a type signature that fixes these type variable(s)
StateTのメソッドであるputをliftで持ち上げて使用しようとしたところ,型があいまいだというエラーが出てしまいました。なぜでしょうか?
runIdentityを付けてListT (StateT s Identity)モナドにして試してみましょう。
Prelude Control.Monad.List Control.Monad.State Control.Monad.Identity> runIdenti ty $ runListT $ runStateT (do{ x <- return 3 `mplus` return 1 `mplus` return 4; guard (x > 2); lift (put 22); return x `mplus` (return $ x+1)}) 12 <interactive>:1:110: No instance for (MonadState t Identity) arising from a use of `put' at <interactive>:1:110-115 Possible fix: add an instance declaration for (MonadState t Identity) In the first argument of `lift', namely `(put 22)' In the expression: lift (put 22) In a 'do' expression: lift (put 22)
今度は「MonadStateクラスにt Identityというインスタンスがない」というエラー・メッセージが出ています。そこで,MonadStateクラスにどんなインスタンスがあるか見てみましょう。
Prelude Control.Monad.List Control.Monad.State Control.Monad.Identity> :i MonadState class (Monad m) => MonadState s m | m -> s where get :: m s put :: s -> m () -- Defined in Control.Monad.State.Class instance (Monad m) => MonadState s (StateT s m) -- Defined in Control.Monad.State.Lazy instance MonadState s (State s) -- Defined in Control.Monad.State.Lazy instance (MonadState s m) => MonadState s (ListT m) -- Defined in Control.Monad.List
ListT mモナドがMonadStateクラスのインスタンスになっているのがわかります。定義を見てみましょう。
instance (MonadState s m) => MonadState s (ListT m) where get = lift get put = lift . put
MonadStateクラスのインスタンスであるモナドmのgetメソッドを,ListTに持ち上げるという定義が書かれています。IdentityモナドはStateモナドの機能を持たないので,当然MonadStateクラスのインスタンスにはなっていません。その結果,上のようなエラーが生じたのです。
したがって,liftを取り除いてモナド変換子ListT StateTのgetメソッドを使うようにすれば正常に動作します。
Prelude Control.Monad.List Control.Monad.State Control.Monad.Identity> runListT $ runStateT (do{ x <- return 3 `mplus` return 1 `mplus` return 4; guard (x > 2); put 22; return x `mplus` (return $ x+1)}) 12 [(3,22),(4,22),(4,22),(5,22)] Prelude Control.Monad.List Control.Monad.State Control.Monad.Identity> runIdentity $ runListT $ runStateT (do{ x <- return 3 `mplus` return 1 `mplus` return 4;guard (x > 2); put 22; return x `mplus` (return $ x+1)}) 12 [(3,22),(4,22),(4,22),(5,22)]
このように「モナド変換子同士の組み合わせに対して自動的にliftを適用する」という仕組みが用意されているのは,使用するモナド変換子ごとにいちいちliftを付けなくてもよくするためです。Control.Monad.Transモジュールには,liftを使ってIOモナドの演算を自動的に最後まで持ち上げていくための型クラスも用意されています。
class (Monad m) => MonadIO m where liftIO :: IO a -> m a instance MonadIO IO where liftIO = id instance (MonadIO m) => MonadIO (StateT s m) where liftIO = lift . liftIO instance (MonadIO m) => MonadIO (ListT m) where liftIO = lift . liftIO
最終的にliftIOはIOモナドの内部でidになることで,I/Oアクションとして正常に動作することが保障されます。I/Oアクションを使う際にliftIOを用いるようにすれば,モナド変換子がどんなに重なっていても気にせずに済みます。
Prelude Control.Monad.List Control.Monad.State> runListT $ runStateT (do{ x <- return 3 `mplus` return 1 `mplus` return 4; guard (x > 2); y <- get; liftIO (print y); put 22; return x `mplus` (return $ x+1)}) 12 12 12 [(3,22),(4,22),(4,22),(5,22)] Prelude Control.Monad.State Control.Monad.List> runListT $ runStateT (do{ x <- return 3 `mplus` return 1 `mplus` return 4; guard (x > 2); put 22; y <- return x `mplus` (return $ x+1); liftIO (print y); return y}) 12 3 4 4 5 [(3,22),(4,22),(4,22),(5,22)] Prelude Control.Monad.List Control.Monad.State> runStateT (put 33.1 >> get >>= \x -> liftIO (print x) >> return x) 22 33.1 (33.1,33.1)
処理の抜け穴unsafeIOTo**を利用する 今回の話から,モナドを提供するならモナド変換子の形で提供したほうがよいことがわかります。しかし,実際には全てのモナドがモナド変換子の形で提供されているわけではありません。Maybeのように単にモナド変換子が提供されていない場合もあれば,STMのようにIOとの組み合わせを回避するために必然的にモナド変換子を提供できないものもあります。 後者の場合にトレースを行うにはどうすればいいでしょうか? 実は,STMモナドなどの一部のモナドには,こうした処理を行うための抜け穴が用意されています。それがunsafeIOto**という名前の関数です。例えば,STMモナドにはGHC.ConcモジュールでunsafeIOToSTMという名前の関数が,STモナドにはunsafeIOToSTという名前の関数が用意されています。 Prelude GHC.Conc> :t unsafeIOToSTM unsafeIOToSTM :: IO a -> STM a Prelude GHC.Conc> :m Control.Monad.ST Prelude Control.Monad.ST> :t unsafeIOToST unsafeIOToST :: IO a -> ST s a これらの関数は,unsafePerformIOなどに似た名前の型を持つことから推測できるように,本来は回避したいはずのI/Oアクションを持ち込むものです。取り扱いには慎重さが必要です。ただ,トレースを行うだけなら,Debug.Traceと同様に,副作用を伴わない処理のように扱って大丈夫でしょう。また,モナド内で扱うのに適した型になっているため,Debug.Traceを使うよりも書きやすくなっています。加えて,専用の関数であるためDebug.Traceを使うよりも適切な動作を期待できます。 このような理由から,モナド変換子の代わりにunsafeIOTo**のような関数が存在する場合には,Debug.TraceよりもunsafeIOTo**を使ってトレースを行うことを薦めます。ただ,unsafeIOTo**が危険性をはらんだ関数であることをくれぐれも忘れないでください。トレース以外の用途に使おうとすると痛い目にあうでしょう。 また,STMをトレースする場合には,並行プログラミング特有の問題にも注意する必要があります。並行プログラミングでは,トレースを行うだけでもタイミングが変わって動作が異なることがありえます。トレースしたときに見えるタイミングに基づいてプログラミングすると,状態表示用の関数を取り払ったときに正常に動作しなくなる可能性があります。 |
著者紹介 shelarcy Haskellの利点を語る際には,バグの原因になる副作用の排除や誤ったプログラムを書かせないための強い静的型付けが引き合いに出されます。でも,Haskellの開発・研究に携わっている人々は,こうした利点だけに満足しているわけではありません。 こうした人々は,現時点でのHaskellの限界を見極め,現実のプログラミングで起こる問題に対処するためのツールの整備や拡充に努めています。ただ,そうした情報がHaskellでプログラミングをしている方々に伝わらなければ意味がありません。Haskellではテストやデバッグが難しいといった偏見を取り払うためにも,またツールに親しんでもらうためにも,テストやデバッグのためのツールを紹介する必要があると思っています。 とはいえ,Haskellのテスト/デバッグ用のツールは,処理系に付属しているものだけでも豊富にあり,ライブラリとして実装されているものはさらに無数にあります。これらのツールを全て紹介することはさすがにできないので,取捨選択して今後紹介していくつもりです。 |