随伴を使って理解するStateモナドの実装
前回の記事は魔法のように見えるStateモナドの実装も、順を追って見ていけば理解することは難しくないという話でした。
しかし状態の変更を順番に処理するというような手続き的な考え方にかなり近い構造が、うまくモナドになってくれるというのは少し不思議ですよね。
この記事では
- タプル
(a, b)
- 関数
a -> b
- カリー化
curry :: ((a, b) -> c) -> a -> b -> c
uncurry :: (a -> b -> c) -> (a, b) -> c
といったHaskellの基本的な要素が随伴と呼ばれる関係を構成することを見て、
その随伴からStateモナドが導かれることを説明していきたいと思います。
随伴
二つの圏
もし
今、
さらに自然同型であるという仮定も弱めて、二つの自然変換
これらの関係を満たす時、
と表します。随伴を構成する2つの自然変換
そろそろ抽象的な話にも疲れてきたので、ここで随伴をHaskellで実装することにしましょう。2つの関手(Functor
)が随伴(Adjunction
)の関係にあることは自然変換(unit
, counit
)の存在によって定まるので、これを型クラスを使って表現しましょう。
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE MultiParamTypeClasses #-}
class (Functor f, Functor g) => Adjunction f g | f -> g, g -> f where
unit :: a -> g (f a)
counit :: f (g a) -> a
自然変換は多相関数を使って表現しています。この実装では恒等関手Identity
が明示的に現れていないことにも注意して下さい。同様の実装はHaskellのライブラリadjunctionsにも見ることができます。
ところで随伴にはもう一つ同値な定義があります。2つの(局所小)圏
この定義が前述の自然変換
という同型が存在することが分かります。この同型により恒等射
という同型を考えて恒等射
このように定めた
1つ目の三角等式
を示すために圏
この図式は自然同型であることより可換になります。
左上の
となり、反対に右上を経由して右下に移すと
となり図式が可換であることより
が分かります。これは任意の対象について成り立つので1つ目の三角等式が成り立つことが分かりました。2つ目の三角等式も同様に示すことができます。
さて、ここからはHaskellの実装に戻りましょう。随伴の別の定義を知ったのでAdjunction
の実装に反映してみましょう。
class (Functor f, Functor g) => Adjunction f g | f -> g, g -> f where
unit :: a -> g (f a)
counit :: f (g a) -> a
leftAdjunct :: (f a -> b) -> a -> g b
rightAdjunct :: (a -> g b) -> f a -> b
unit = leftAdjunct id
counit = rightAdjunct id
leftAdjunct
とrightAdjunct
は自然同型における全単射を表しています。これらに恒等射id
を与えて評価することでunit
, counit
が出てくるのは上述した証明の流れと対応していてとても綺麗ですね!
随伴から導かれるモナド
実は随伴関手が与えられると、そこからモナドを構成することができます。
まずはモナドの定義を思い出しておきましょう。
圏
結論から言えば随伴関手
と定めます。
これらがちゃんとモナドになっていることを確認してみましょう。
まず1つ目の可換図式は
となりますが
この可換図式を
また2つ目の可換図式は
となり、随伴の三角等式を関手
それでは随伴からモナドを構成する方法をHaskellで実装していきましょう。
HaskellでFunctor
を合成する方法はData.Functor.Compose
で提供されていますが、インスタンス定義が重複してエラーになってしまうので今回は自分で作ります。
newtype (.) g f a = Compose { getCompose :: g (f a) }
.
をFunctor
の合成を表すType Operatorとして使うことでFunctor g
とf
の合成を g . f
と分かりやすく書くことができます[2]。
HaskellでMonad
のインスタンスを作るためにはFunctor
とApplicative
のインスタンスにする必要があります。
まずFunctor
の実装は簡単です。
instance (Functor f, Functor g) => Functor (g . f) where
fmap f (Compose gf) = Compose $ fmap (fmap f) gf
次にApplicative
の実装ですが、その前にモナドであることを先取りして自然変換join
を定義しておきましょう。
join :: Adjunction f g => g (f (g (f a))) -> g (f a)
join = fmap counit
join' :: Adjunction f g => (g . f) ((g . f) a) -> (g . f) a
join' (Compose gfgf) = Compose . join $ fmap (fmap getCompose) gfgf
join'
は実装の詳細を吸収して複雑になってますが、実態はjoin
です。これはcounit
、すなわちfmap
する実装になっており、圏論で随伴からモナドを導くときの構成と同じになっていることが分かります。
これを使えばApplicative
は以下のように実装できます。
instance Adjunction f g => Applicative (g . f) where
pure = Compose . unit
f <*> k = join' $ fmap (\a -> fmap ($ a) f) k
pure
、つまりモナドにおける自然変換unit
がそのまま対応しています。
最後にMonad
のインスタンスを実装しましょう。
instance Adjunction f g => Monad (g . f) where
f >>= m = join' $ fmap m f
これで完成です!
随伴Adjunction
であることを利用してモナドMonad
の実装を行うことができました👏
Stateモナド
さて、いよいよ本題であるStateモナドの実装を見ていきましょう。
そのためにもちろん随伴になっている関手を使います。
実は圏論ではよく知られた積とべき対象の随伴から導かれるモナドがStateモナドに対応しています。Haskellの言葉で言えばタプル(a, b)
と関数a -> b
です。Writer
とReader
と言っても良いでしょう。
そしてこれらの随伴を定めるleftAdjunct
とrightAdjunct
がカリー化とその逆に対応しているのです。
実際に実装を見てみましょう。
curry' :: ((b, a) -> c) -> a -> b -> c
curry' f a b = f (b, a)
uncurry' :: (a -> b -> c) -> (b, a) -> c
uncurry' f (b, a) = f a b
instance Adjunction ((,) b) ((->) b) where
leftAdjunct = curry'
rightAdjunct = uncurry'
Haskellではタプルの右側だけ型を適応した型を手軽に作れないので、引数の順番が少し違うcurry
やuncurry
を再実装していますが本質は変わりません[3]。
ところでもうStateモナドの実装が終わっていることにお気づきでしょうか?
随伴関手が定まればモナドは自動的に手に入るので、
type State s = ((->) s) . ((,) s)
これで終わりです!
前回定義したStateモナドの型が
newtype State s a = State { runState :: s -> (s, a) }
であったことを思い出すと確かに対応していますね。
もちろんget
やset
を定義して普通のStateモナドのように使うことも可能です。
最後に等式論証を使って随伴を使ったモナドの実装が手続き的な実装と対応していることを確認して終わりましょう。少し長いですが丁寧に展開していきます。
f >>= m = join' $ fmap m f
<=> {- join' を展開 -}
f >>= m = (\(Compose gfgf) -> Compose . join $ fmap (fmap getCompose) gfgf) $ fmap m f
<=> {- ラムダ式を適用 -}
f >>= m = Compose . join $ fmap (fmap getCompose) (getCompose $ fmap m f)
<=> {- f = Compose $ \s -> getCompose f s -}
f >>= m = Compose . join $ fmap (fmap getCompose)
(getCompose $ fmap m (Compose $ \s -> getCompose f s))
<=> {- getCompose f s = let (s', a) = getCompose f s in (s', a) -}
f >>= m = Compose . join $ fmap (fmap getCompose)
(getCompose $ fmap m (Compose $ \s -> let (s', a) = getCompose f s in (s', a)))
<=> {- fmap m を評価 -}
f >>= m = Compose . join $ fmap (fmap getCompose)
(getCompose $ (Compose $ \s -> let (s', a) = getCompose f s in (s', m a)))
<=> {- getCompose . Compose = id -}
f >>= m = Compose . join $ fmap (fmap getCompose)
(\s -> let (s', a) = getCompose f s in (s', m a))
<=> {- join = fmap counit = fmap (rightAdjunct id) = fmap (uncurry' id) = fmap (\(b, f) -> f b) -}
f >>= m = Compose . fmap (\(b, f) -> f b) $ fmap (fmap getCompose)
(\s -> let (s', a) = getCompose f s in (s', m a))
<=> {- fmap 融合則 -}
f >>= m = Compose $ fmap ((\(b, f) -> f b) . (fmap getCompose))
(\s -> let (s', a) = getCompose f s in (s', m a))
<=> {- fmap 適用 -}
f >>= m = Compose $ \s ->
let (s', a) = getCompose f s
in (\(b, f) -> f b) . (fmap getCompose) $ (s', m a)
<=> {- inの中を評価 -}
f >>= m = Compose $ \s ->
let (s', a) = getCompose f s
in getCompose (m a) s'
前回のモナドの実装と比較してみると見事に対応していることが分かりますね👏
instance Monad (State s) where
f >>= m = State $ \s ->
let (s', a) = runState f s
in runState (m a) s'
あとがき
随伴を使ってStateモナドが定義できることを確認し、手続き的な実装とちゃんと対応することを見てきました。随伴に纏わる面白い話はまだまだあるので更に知りたい人は以下のリンクからたどると良いでしょう。
- From Adjunctions to Monads 随伴からモナドの導出をストリング図など交えて解説されています
- 随伴がモテないのはどう考えてもモナドが悪い!(モナドとコモナドの関係が分かる話) 実は随伴関手を反対向きに組み合わせるとコモナドを導出することができます。Stateモナドの反対はStoreコモナド
- Haskellと随伴 タプルと関数以外にも興味深い随伴が紹介されています
\読んでいただきありがとうございました!/
この記事が面白かったら いいね♡ をいただけると嬉しいです☺️
バッジを贈っていただければ次の記事を書くため励みになります🙌
-
定義が同値であることを見るには反対向きの証明も必要ですが、長くなるので気になる人はWikipediaを参照して下さい(4.2 余単位-単位随伴がhom集合随伴を導くこと)。 ↩︎
-
.
がType Operatorとして使えるようになったのはGHC8.8.1からのようです(Haskell-jpもくもく会で教えてもらいました) ↩︎ -
前回の記事でもStateモナドの型がtransformersとズレたのはこれが原因です😅 ↩︎
Discussion