みどりねこ日記

よくわからないけど、頑張りますよ。

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)

さて、前回の記事では>>=演算子を実装しました。しかし、実際のところ、何が起きているのでしょうか。
(>>=)演算子は、サンドイッチの層を一枚ずつ剥がし、中の値に関数を適用し、その結果である新たな値を新しいサンドイッチの中に詰めて返すのだと考えても良いです。

  1. サンドイッチから値を引っ張りだす

MaybeTコンストラクタについては一旦忘れましょう。$のあとにつづく処理はmモナドの中で実行されているのであり、MaybeTモナドの中ではないことに注意してください。
最初に、runMaybeT tmb_vを呼ぶことでサンドイッチの一番上を取り除きます。
次に、(>>=)演算子を使うことでサンドイッチの2層目を取り除きます。この処理はmモナドの中で行われています。
最終的に、caseとパターンマッチングを使用して、サンドイッチの最下層(値)をはがしてきます。

  1. サンドイッチに詰める

もし最下層が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つの層を取り出すために必要な部分です。

Haskellモナドトランスフォーマー(5)

モナドトランスフォーマーがいかにして動いているのかは、どのように>>=演算子が実装されているかを理解するとわかるでしょう。>>=演算子の実装は、トランスフォーマーではない、もとのモナドのそれと非常に似ていることがわかると思います。

トランスフォーマーの型コンストラクタについて説明します。型コンストラクタはHaskellモナドの基幹となるものです。Reader r aが、環境rのついたReaderモナドの中にある型aの値の型であることを思い出してください。型コンストラクタReader rはMonadクラスのインスタンスで、runReader :: Reader r a -> r -> aはReaderモナドの演算を行い、型aである結果を返します。

Readerモナドトランスフォーマー版は、ReaderTと呼ばれ、モナド型コンストラクタを新たなパラメータとして付加します。ReaderT r m aは、”元のモナドがReaderで、内側のモナドがmであるような合成モナド”の値の型です。

Reader T r mはモナドクラスのインスタンスで、関数runReaderT :: ReaderT r m a -> r -> m aは合成モナドを実行し、型m aである結果を返します。