Haskellモナドトランスフォーマー(12)
今まで、MaybeTとListTという2つの非常に単純なモナドトランスフォーマーの実装をしてきました。それから、少し遠回りをしてモナドをそれのトランスフォーマーに持ちあげるという話もしましたね。ここでは、その二つの考えを、StateTという標準ライブラリの中でも興味深いトランスフォーマーの実装を見ながらつなげてみましょう。このトランスフォーマーを学ぶことで、モナドトランスフォーマーをコードの中で使用するときに、トランスフォーマーのメカニズムがはっきりと見えてくるようになります。もしもわかりにくかったら、過去のStateモナドについての記事を読んでみてください。
Stateモナドが
newtype State s a = State { runState :: (s -> (a, s)) }
と定義されているように、StateTトランスフォーマーも
newtype StateT m a = StateT { runStateT :: (s -> m (a, s)) }
と定義されています。
State sはMonadクラスとMonadState sクラスのどちらのインスタンスでもあります。なので、StateT s mもMonadとMonadState sクラスのメンバーですし、更にいうと、もし、mがMonadPlusのインスタンスならStateT s mもまたMonadPlusのメンバーであるべきです。
StateT s mをMonadのインスタンスとする実装は以下のようになります。
State |
---|
newtype State s a = State { runState :: (s -> (a, s)) } instance Monad (State s) where return a = State $ \s -> (a, s) (State x) >>= f = State $ \s -> let (v, s') = x s in runState (f v) s'
StateT |
---|
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 runStateT (f v) s'
このreturnの定義は、内側のモナドのreturnを使用し、それを結果とし、>>=演算子は内側のモナドの計算をするために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)
MonadPluのインスタンスとするために、StateTがMonadPlusとともに使用される合成モナドの定義もしましょう。
instance (MonadPlus m) => MonadPlus (StateT s m) where mzero = StateT $ \s -> mzero (StateT x1) `mplus` (StateT x2) = StateT $ \s -> (x1 s) `mplus` (x2 s)
最後に、StateTモナドをMonadTransクラスのインスタンスとしてliftもつけ、Haskellのモナドクラスとして完成させましょう。
instance MonadTrans (StateT s) where lift c = StateT $ \s -> c >>= (\x -> return (x, s))
関数liftは、内側のモナドの計算を、入力された状態に結びつける関数に結びつける、状態変換関数StateTを作り出します。結果として、例えばStateTをListモナドに適応した時、結果としてリストを返す関数(Listモナド内の計算)は、StateT s [ ]に持ち上げられ、StateT (s -> [ (a, s) ]を返す関数になります。持ち上げられた計算は複数の (値, 状態) というペアを入力より生成し返します。
この結果として、StateTの計算をコピーして、持ち上げられた計算によって返されたリスト内のそれぞれの計算結果に対して違う計算を生成すます。もちろん、StateTを違うモナドに対して適応するのは違うセマンティクスのliftを生成します。
Haskellモナドトランスフォーマー(11)
liftの実装はわりと素直です。MaybeTトランスフォーマーの場合を見てみます。
instance MonadTrans MaybeT where lift mon = MaybeT (mon >>= return . Just)
内側のモナドを値に適用し、サンドイッチで言う真ん中までの処理を行ったあと、>>=演算子と型コンストラクタを使用して最下層のモナドを真ん中のスライスの下に潜り込ませます。その後、MaybeTコンストラクタを使用してサンドイッチを完成させます。関数liftを使用することで、3つの層をもつモナドサンドイッチに変換することができます。
liftMがどの型に対しても適用できる関数であるのに対して、liftが個別の定義が必要である理由を考えてみてください。
Haskellモナドトランスフォーマー(10)
モナドトランスフォーマーによって合成されたモナドを使うとき、内側のモナドをきちんと考える必要がないようにしたいところです。そのほうが綺麗でシンプルなコードになるからです。内側のモナドの型を持った値を扱う計算の中でdoブロックを使うより、内側のモナドを合成モナドまで持ち上げる命令を使ったほうが良いでしょう。
liftMは、モナディックでない関数をモナドに持ち上げる関数だったことを思い出してください。各モナドトランスフォーマーはモナディックな計算を合成モナドに持ち上げる関数liftを用意しています。
MonadTransクラスはControl.Monad.Transの中で定義されており、liftという関数1つだけを提供します。
class MonadTrans t where lift :: (Monad m) => m a -> t m a
IO処理を持ち上げるために最適化されたモナドはMonadIOクラスのメンバーとして定義されており、関数liftIOと名付けられています。
class (Monad m) => MonadIO m where liftIO :: IO a -> m a
Haskellモナドトランスフォーマー(9)
標準ライブラリの中でも、とりわけ使いやすい関数の一つに、liftMがあります。モナディックでない関数をモナドの関数に持ち上げます。型を見てみましょう。
liftM :: Monad m => (a1 -> r) -> m a1 -> m r
さて、liftMは関数(a1 -> r)と、a1を保持したモナドをとり、受け取った関数をa1に適用し、mに包んで結果を返します。この関数を理解するには、どのように使われるかを見るのが一番です。以下のコードはすべて同じ意味を持ちます。
do記法 |
---|
do foo <- someMonadicThing return (myFn foo)
liftM |
---|
liftM myFn someMonadicThing
liftM(演算子として) |
---|
myFn `liftM` someMonadicThing
3つ目の例は目からうろこなのでは。liftMは($)のモナドバージョンとして使われています。
モナディックでないもの |
---|
myFn $ aNonMonadicThing
モナド |
---|
myFn `liftM` someMonadicThing
Haskellモナドトランスフォーマー(8)
次はリストトランスフォーマーを考えましょう。
Maybeトランスフォーマーのように、1つ引数をとるコンストラクタ付きのデータ型を定義します。
newtype ListT m a = ListT { runListT :: m [a] }
ListTモナドの実装も、もとのListモナドのそれと非常に似通っています。
Maybeのものと比べると内側のモナドに対する処理が少し増えていますが、基本的は同じで、モナドのサンドイッチ(ListT - m - List)をはがして新たに生成します。
List |
---|
instance Monad [] where b_v >>= f = let x = map f b_v in concat x
ListT |
---|
instance (Monad m) => Monad (ListT m) where tmb_v >>= f = ListT $ runListT tmb_v >>= \b_v -> mapM (runListT . f) b_v >>= \x -> return (concat x)
Haskellモナドトランスフォーマー(7)
さて、前回の記事では>>=演算子を実装しました。しかし、実際のところ、何が起きているのでしょうか。
(>>=)演算子は、サンドイッチの層を一枚ずつ剥がし、中の値に関数を適用し、その結果である新たな値を新しいサンドイッチの中に詰めて返すのだと考えても良いです。
- サンドイッチから値を引っ張りだす
MaybeTコンストラクタについては一旦忘れましょう。$のあとにつづく処理はmモナドの中で実行されているのであり、MaybeTモナドの中ではないことに注意してください。
最初に、runMaybeT tmb_vを呼ぶことでサンドイッチの一番上を取り除きます。
次に、(>>=)演算子を使うことでサンドイッチの2層目を取り除きます。この処理はmモナドの中で行われています。
最終的に、caseとパターンマッチングを使用して、サンドイッチの最下層(値)をはがしてきます。
- サンドイッチに詰める
もし最下層がNothingだったなら、単純にreturn Nothingをします(2層のサンドイッチになります)。この値はMaybeTコンストラクタによって層を付加され完全なサンドイッチとなります。
もし最下層がJust vだったなら、その値に関数fを適用します。ただし、ここで問題が発生します。fをvに適用すると、3層の完全なサンドイッチを生成しますが、それにMaybeTを新たに適用することになり、4層になるので型エラーが発生します。
どのように回避すればよいでしょうか。
最初にrunMaybeTを走らせることで上の層を剥ぎとっておくことで、MaybeTコンストラクタが適用できるようにすればよいですね。
Haskellモナドトランスフォーマー(6)
Maybeトランスフォーマーのデータ型を宣言するところから始めましょう。MaybeTコンストラクタはひとつ引数を取ります。トランスフォーマーは、元のモナドと同じデータを取るので、newtypeキーワードを使用することにします。dataキーワードを使ってもいいのですが、必要のない負荷がかかるのでやめておきましょう。
newtype MaybeT m a = MaybeT { runMaybeT :: m (Maybe a) }
最初は戸惑うかもしれませんが、見た目ほど複雑でもないです。MaybeTのコンストラクタは、型m (Maybe a)である値をひとつ引数に取ります。ただそれだけです。このレコードの部分はただの糖衣構文で、MaybeTがレコードのように見ることができるというのと、runMaybeTを呼ぶことで引数にとった値を取得できます。
わかりやすく考えるなら、モナドトランスフォーマーをサンドイッチとして考えましょう。サンドイッチの下が元のモナド(ここではMaybe)、中身が「内なるモナド」m、そして一番上がモナドトランスフォーマーMaybeTです。関数runMaybeTの存在意義は、この一番上のモナドトランスフォーマーMaybeTを取り除くことです。runMaybeTの型は(MaybeT m a) -> m (Maybe a)です。
改めて言いますが、モナドトランスフォーマー自身もモナドです。MaybeTモナドの実装の一部が下にあります。この実装を見れば、元のモナドであるMaybeがいかにシンプルな実装であるかがわかりと思います。
Maybe |
---|
instance Monad Maybe where b_ v >>= f = case b_v of Nothing -> Nothing Just v -> f v
MaybeT |
---|
instance (Monad m) => Monad (MaybeT m) where tmb_v >>= f = MaybeT $ runMaybeT tmb_v >>= ¥b_v -> case b_v of Nothing -> return Nothing Just v -> runMaybeT $ f v
MaybeTの実装がMaybeのbindの実装に似ていると気づくかもしれません(ただし例外処理多し)。この余分な実装がモナドのサンドイッチから2つの層を取り出すために必要な部分です。