前回、ストリング図を使ってStateT
モナド変換子のモナド則を証明しました。
今度はReaderT
,WriterT
,ExceptT
の3つについてやっていきます。
共通の構造
前回の記事までは、証明の方針は私が天下り的に与えていましたが、 今回は「発見的な」やり方で説明してみようと思います。
WriterTの場合
まずは、WriterT
の実装を眺めてみます。
-- transformers パッケージのものとは(同型ですが)少し違います
newtype WriterT w m a = WriterT { runWriterT :: m (w, a) }
instance Functor m => Functor (WriterT w m) where {- 省略 -}
instance (Monoid w, Monad m) => Monad (WriterT w m) where
pure = pureWriter >>> pure >>> WriterT
where
pureWriter :: Monoid w => a -> (w, a)
= (mempty, a) -- Writerモナド (w,_) のpure
pureWriter a = fmap runWriterT >>> runWriterT >>> joinT >>> WriterT
join where
joinT :: (Monad m, Monoid w) => m (w, m (w, a)) -> m (w, a)
=
joinT mwmwa do (w, mwa) <- mwmwa
<- mwa
(w', a) return (w <> w', a)
ここで、pure
は自然変換pureWriter
, pure
, WriterT
の合成で書くことができていて、
ストリング図に書きやすそうです。
一方、join
の実装に使ったjoinT
は、もっと単純な自然変換の合成で書けてはいないようです。
joinT
をうまいこと自然変換たちに分解してみましょう。
-- Writerモナド(w,_)のjoinが使えそうです
joinWriter :: Monoid w => (w, (w, a)) -> (w, a)
= (w <> w', a)
joinWriter (w, (w', a))
joinT :: (Monad m, Monoid w) => m (w, m (w, a)) -> m (w, a)
= \mwmwa -> do
joinT <- mwmwa
(w, mwa) <- mwa
(w', a) return (w <> w', a)
= \mwmwa -> do
<- mwmwa
(w, mwa) fmap (\(w',a) -> (w <> w', a)) m
= \mwmwa -> mwmwa >>= \(w,mwa) -> mwa >>= \(w',a) -> return (w <> w', a)
= \mwmwa -> mwmwa >>= \(w,mwa) -> fmap (\(w',a) -> (w<>w',a)) mwa
= fmap (\(w,mwa) -> fmap (\(w',a) -> (w<>w', a)) mwa) >>> join
-- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-- ここで、この関数に注目してみます。
-- この関数の型は (w, m (w, a)) -> m (w, a) です。
-- また、関数の中身がjoinWriterに似ていますが、
-- そのままではjoinWriterで置き換えられる部分がありません。
--
-- なのでまず (w, m (w, a)) の外側のWriterモナドとmを入れ替え、
-- fmap joinWriter :: m (w, (w, a)) -> m (w, a) と合成するとよさそうです。
= fmap (swapWM >>> fmap joinWriter) >>> join
= fmap swapWM >>> fmap (fmap joinWriter) >>> join
where swapWM :: Functor m => (w, m b) -> m (w, b)
= fmap (\b -> (w,b)) mb swapWM (w, mb)
めでたく、WriterT
のjoin
も単純な自然変換の合成で書けました。まとめると、
-- Writerモナド (w, )のpureとjoin
pureWriter :: Monoid w => a -> (w, a)
= (mempty, a)
pureWriter a
joinWriter :: Monoid w => (w, (w, a)) -> (w, a)
= (w <> w', a)
joinWriter (w, (w', a))
-- Writerとmの入れ替え
swapWM :: Functor m => (w, m b) -> m (w, b)
= fmap (\b -> (w,b)) mb
swapWM (w, mb)
instance (Monoid w, Monad m) => Monad (WriterT w m) where
pure = pureWriter >>> pure >>> WriterT
= fmap runWriterT -- WriterT w m (WriterT w m a) -> WriterT w m (m (w, a))
join >>> runWriterT -- WriterT w m (m (w, a)) -> m (w, m (w, a))
>>> fmap swapWM -- m (w, m (w, a)) -> m (m (w, (w, a)))
>>> fmap (fmap joinWriter) -- m (m (w, (w, a))) -> m (m (w, a))
>>> join -- m (m (w, a)) -> m (w, a)
>>> WriterT -- m (w, a) -> WriterT w m a
これをストリング図に描いてみます。
青丸をWriterT w m
のモナド演算(pure
またはjoin
)に当てていて、
同様に緑丸をWriter
モナド、赤丸をモナドm
に対応させています。
また、二つの線が交わるところにある点がswapWM
です。
さて、私達はWriterT
を構成する2つのモナド(w,_)
とm
のモナド演算については
よくわかっていますが、自然変換swapWM
についてはよくわかっていません。
そのため、ひとまずWriterT
のモナド則の証明は後回しにしておきます。
ReaderTの場合
ReaderT
モナド変換子をストリング図に描く方法を考えます。
newtype ReaderT r m a = ReaderT { runReaderT :: r -> m a }
instance Functor m => Functor (ReaderT r m) where {- 省略 -}
-- WriterTはWriterモナドを部品として作ることができました。
-- ReaderTもそうなって欲しいですね。
pureReader :: a -> (r -> a)
= const
pureReader joinReader :: (r -> r -> a) -> (r -> a)
= f r r
joinReader f r
instance (Monad m) => Monad (ReaderT r m) where
pure = pure >>> pureReader >>> ReaderT
= fmap runReaderT >>> runReaderT >>> joinT >>> ReaderT
join where
joinT :: (Monad m) => (r -> m (r -> m a)) -> r -> m a
= \r -> rmrma r >>= \rma -> rma r joinT rmrma
ふたたび、joinT
が単純な自然変換の合成になっていませんが、
WriterT
のときと同じ作戦でいってみましょう。
WriterT w m
がWriter w = (w, )
とm
の合成Functor
だったように、
ReaderT r m
もm
とReader r = (r -> _)
の合成Functor
です。
そのため、joinT
の引数はFunctor
の4段重ねr -> m (r -> (m _))
になっています。
4段重ねの中央の2段、m (r -> _)
を入れ替えられれば、Reader
とm
のjoin
を使えます。
-- mとReaderの入れ替え
swapMR :: (Functor m) => m (r -> b) -> r -> m b
= fmap ($ r) mrb
swapMR mrb r
-- swapMRで中央を入れ替える方針で型を合わせるパズルをすれば、
-- joinTと同じ型の自然変換が見つかります。
joinT' :: (Monad m) => (r -> m (r -> m a)) -> r -> m a
= fmap swapMR >>> fmap (fmap join) >>> joinReader
joinT'
-- joinT' = joinT を確認します。
joinT' rmrma= joinReader $ fmap (fmap join) $ fmap swapMR rmrma
= joinReader $ fmap (fmap join) $ \r -> swapMR (rmrma r)
= joinReader $ fmap (fmap join) $ \r r' -> fmap ($ r') (rmrma r)
= joinReader $ \r r' -> join $ fmap ($ r') (rmrma r)
= \r -> join $ fmap ($ r) (rmrma r)
= \r -> rmrma r >>= ($ r)
= \r -> rmrma r >>= \rma -> rma r
= joinT rmrma
これをもとにストリング図を描くと、WriterT
のものとラベル以外同じものが出来上がります。
ExceptTの場合
ExceptT
もReaderT
やWriterT
と”同じ構造”を持っています。
もはや細かい説明は不要かと思いますが、ExceptT e m
はモナドm
とモナドEither e
の合成Functor
で、
入れ替えをする自然変換swapEM
によってモナドにしています。
newtype ExceptT e m a =
ExceptT { runExceptT :: m (Either e a) }
swapEM :: Monad m => Either e (m b) -> m (Either e b)
Left e) = pure (Left e)
swapEM (Right mb) = fmap Right mb swapEM (
3つをひとまとめにする
WriterT
、ReaderT
、ExceptT
はどれも、
モナドの合成順を入れ替える自然変換を使って定義できました。
この3つのどれも、以下の構造を持っています。
あるモナド
m
とモナドn
の合成m ∘ n
になっている入れ替えをする自然変換
swap ::∀a. n (m a) -> m (n a)
を持っているswap
を使って、pure
とjoin
が下図のように定義されている
モナド変換子 | 外側のモナド | 内側のモナド | swap |
---|---|---|---|
WriterT w m |
m |
(w,_) |
swapWM :: (w, m b) -> m (w, b) |
ReaderT r m |
r -> _ |
m |
swapMR :: m (r -> b) -> r -> m b |
ExceptT e m |
m |
Either e |
swapEM :: Either e (m b) -> m (Either e b) |
モナドの合成のモナド則
任意のモナドの組み合わせm, n
と前節のswap
によって、
m ∘ n
がモナドになると言いたいところですが、どんな自然変換でもよいのでしょうか?そんなことは無さそうです。
実際、なんでもOKではありません。WriterT
の構成にあるswapWM :: (w, m a) -> m (w, a)
で、
モノイドw
を勝手にmempty
に置き換えたりすれば、
出来上がったWriterT w m
はモナド則を満たさなくなってしまいます。
しかし、「swap
が特定の条件を満たせばm ∘ n
がモナドになる」とは言えるはずです。
ストリング図を描いて、どんな条件を満たせばよいのか考えてみます。
まず、左単位法則から見てみます。
ピンク色で強調した部分が成り立っているかどうかわからない等式で、
それ以外の部分はm
,n
のモナド則といった、すでに知っている等式です。
この成り立ってほしい等式(1)は、もっと単純なバージョン(下図右辺)と同値になります。
右単位法則が成り立つ条件から、同様に次の成り立ってほしい等式(2)が出てきます。
最後に結合法則を見てみます。(結合法則が成り立つために成り立ってほしい等式を、 ピンク色で強調しています。)
成り立ってほしい等式だけ取り出せば、次のようになります。
これらの等式(1)-(4)を満たすようなswap
であれば、m ∘ n
がモナドになると言えます。
等式(1)-(4)を、ストリング図からHaskellの式に書き直すと、次のようになります。
instance Monad m
instance Monad n
swap :: ∀b. n (m b) -> m (n b)
-- 等式(1)
pure >>> swap = fmap pure :: ∀b. m b -> m (n b)
-- 等式(2)
fmap pure >>> swap = pure :: ∀b. n b -> m (n b)
-- 等式(3)
>>> swap :: ∀b. n (n (m b)) -> m (n b)
join = fmap swap >>> swap >>> fmap join
-- 等式(4)
fmap join >>> swap :: ∀b. n (m (m b)) -> m (n b)
= swap >>> fmap swap >>> join
自然変換swap
によって(1)-(4)が成り立つことを、
「swap
は、モナドm
がモナドn
に対する1分配である2」と呼ぶことにして、
ここで証明できたことをまとめると、次のようになります。
モナド
m
がモナドn
に対する分配swap
を持てば、swap
を使ってm ∘ n
がモナドになる
これをWriterT
の場合に適用すれば、WriterT w m
がモナド則を満たしていることの証明は、
swap=swapWM
が分配であることを確認すればOKです。
-- 再掲
swapWM :: Functor m => (w, m b) -> m (w, b)
= fmap (\b -> (w,b)) mb
swapWM (w, mb)
-- 等式(1)
>>> swapWM = fmap pureWriter :: ∀b. m b -> m (w, b)
pureWriter >>> swapWM) mb
(pureWriter = swapWM (mempty, mb)
= fmap (\b -> (mempty, b)) mb
= fmap pureWriter mb
-- 等式(2)
fmap pure >>> swapWM = pure :: ∀b. (w, b) -> m (w, b)
fmap pure >>> swapWM $ (w,b)
= swapWM (w, pure b)
= fmap (\b -> (w,b)) (pure b)
= pure (w,b)
-- 等式(3)
>>> swapWM :: ∀b. (w, (w, m b)) -> m (w, b)
joinWriter = fmap swapWM >>> swapWM >>> fmap joinWriter
>>> swapWM $ (w,(w',mb))
joinWriter = swapWM (w <> w', mb)
= fmap (\b -> (w <> w', b)) mb
fmap swapWM >>> swapWM >>> fmap joinWriter $ (w,(w',mb))
= swapWM >>> fmap joinWriter $ (w, swapWM (w', mb))
= fmap joinWriter $ swapWM (w, swapWM (w', mb))
= fmap joinWriter $ fmap (\b -> (w,b)) $ fmap (\b -> (w',b)) mb
= fmap (joinWriter . (\b -> (w,b)) . (\b -> (w',b))) $ mb
= fmap (\b -> joinWriter (w,(w',b))) mb
= fmap (\b -> (w <> w', b)) mb
-- 等式(4)
fmap join >>> swapWM :: ∀b. (w, m (m b)) -> m (w, b)
= swapWM >>> fmap swapWM >>> join
fmap join >>> swapWM $ (w, mmb)
= swapWM (w, join mmb)
= fmap (\b -> (w,b)) $ join mmb
>>> fmap swapWM >>> join $ (w, mmb)
swapWM = fmap swapWM >>> join $ fmap (\mb -> (w,mb)) mmb
= join $ fmap (swapWM . (\mb -> (w,mb))) mmb
= join $ fmap (\mb -> swapWM (w,mb)) mmb
= join $ fmap (\mb -> fmap (\b -> (w,b)) mb) mmb
= join $ fmap (fmap (\b -> (w,b))) mmb
-- join . fmap (fmap f) = fmap f . join
= fmap (\b -> (w,b)) $ join mmb
ReaderT
もExceptT
も、同じようにswapMR
、swapEM
が分配であることが示せて、
モナド則を満たすことがわかります。
(注意)
「モナド
m
がモナドn
に対する……」において、m
とn
の順番には意味があります。swap :: ∀b. n (m b) -> m (n b)
があるからといって、逆向きの自然変換reverseSwap :: ∀b. m (n b) -> n (m b)
があったり、 それが等式(1)-(4)を満たす保証はまったく無いからです。そのため、「モナド
m,n
の……」のように並列で呼ばないようにしました。↩︎「分配」という単語は、これが圏論では「分配律(distributive law)」と呼ばれていることから取っています。
「分配律」から「律」を落としたのは、モナド則の結合法則などの自然変換の間の等式と混同しないためです。 等式(1)-(4)を満たすような
swap
それ自体が「分配律」と呼ばれていて、等式(1)-(4)は「分配律」 の満たすべき性質(言うなれば「分配律律」)です。ややこしすぎて本文に書くことでは無いと思いました。↩︎