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つの層を取り出すために必要な部分です。
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である結果を返します。
Haskellモナドトランスフォーマー(4)
モナドトランスフォーマーはモナドの結合を簡単にするモナドの、特別な種類です。例えば、ReaderT Env IO aはEnv型の環境から読み取り、IOを行い、型aを返すことができる計算です。モナドトランスフォーマーの型コンストラクタはモナドのそれにパラメータ化されており、モナディックな型を生成します。モナドの仕組みがわかっていることを前提に話すので、もしかしたらわかりにくいかもしれません。そういったときは、Haskellのモナド(結構前の記事)あたりを見ていただけたらと思います。
トランスフォーマーは、元のモナドのいとこと見るのがよいです。例えば、ListTモナドは元のモナド、つまりListモナドのいとこです。一般的にモナドトランスフォーマーは、内側にあるモナドに値を渡していくために多少複雑になるだけで、他はそのいとこ(元のモナド)とほぼ同じように実装されています。
モナドテンプレートライブラリの基本的なモナドのそれぞれ全てに、トランスフォーマーが矛盾なく宣言されています。しかし、すべてのモナドが同じトランスフォーメーションを行なってくれるというわけではありません。ContTトランスフォーマーは(a -> r) -> rという継続を(a -> m r) -> m rという継続に変えます。StateTトランスフォーマーは違います。StateTトランスフォーマーはs -> (a, s)という関数を s -> m (a, s)と変えます。より一般的に言うと、モナドのトランスフォーマーを作る要素に魔法の入る余地はありません。それぞれのトランスフォーマーはトランスフォーマーでない型によって構成された文脈に応じて形を変えます。
基本的なモナド | トランスフォーマー | 元の型 | 結合後の型 |
---|---|---|---|
Error | ErrorT | Either e a | m (Either e a) |
State | StateT | s -> (a, s) | s -> m (a, s) |
Reader | ReaderT | r -> a | r -> m a |
Writer | WriterT | (a, w) | m (a, w) |
Cont | ContT | (a -> r) -> r | (a -> m r) -> m r |
上のテーブルでは、ほとんどのトランスフォーマーFooTは元のモナドFooと、結果の型をモナドmで包み込んでいるという点で異なります。Contモナドは2つの”結果”が方の中にありますが、ContTモナドはどちらのつながったモナドも包み込みます。言い方を変えると、基本的にはこれらは以下のような共通するものを持っています。
元の型 | 結合した型 |
---|---|
* | m * |
* -> * | * -> m * |
(* -> *) -> * | (* -> m *) -> m * |
Haskellモナドトランスフォーマー(3)
さて、準備が整ったので、getPasswordやそれを利用するコードを書きなおしてみましょう。
getValidPassword :: MaybeT IO String getValidPassword = do s <- lift getLine guard (isValid s) return s askPassword :: MaybeT IO () askPassword = do lift $ putStrLn "Insert your new password: " value <- getValidPassword lift $ putStrLn "Storing in database ... "
(>>=)演算子のお陰で、Nothingなのか、Justなのかをプログラマが手動でチェックする必要がなくなり、随分とすっきりしていますね。
liftを関数getLineやputStrLnに用いることでMaybeT IOモナドにしています。MaybeT IOはMonadPlusのインスタンスでもあるので、パスワードが正しいかは、パスワードがダメなとき、mzero(つまりIO Nothing)を返してくるguardによってチェックすることが出来ます。
ついでながら、正しいパスワードが設定されるまで、永遠に聞き続けるプログラムも簡単に作れます。
askPassword :: MaybeT IO () askPassword = do lift $ putStrLn "Insert your new password: " value <- msum $ repeat getValidPassword lift $ putStrLn "Storing in database ... "