この記事はHaskell Advent Calendar 2024の10日目です
去年の12月という古い話ではありますが、 @Kory__3 さんが発した疑問(ツイート)に答える形で、次の事実を証明することができました。 (Agdaで書いた証明)
Monad (State s)
のインスタンスは唯一-
Haskellにおいて、
instance Monad (State s) where ...
と書けるMonad
のインスタンスであってMonad
則を満たすものは、 標準的なStateモナドと同値なものに限られる。
ただし、State s
は以下の型とします。
newtype State s a = State { runState :: s -> (s, a) }
この記事では、その証明の概略を述べます。
何を証明したのか
証明の前に、まずは示そうとしていることが正確に何であるのかを詳しく述べます。
「instance Monad (State s) where ...
と書けるインスタンス」というのは、
文字通り次の形をしたMonad
のインスタンスです。
instance Monad (State s) where
return = ...
>>=) = ... (
このように書くことができるインスタンスのうち、きちんとMonad
則を満たすものは、
以下に示す”普通の”Stateモナドだけしか存在しないことを示すことが証明のゴールになります。
instance Monad (State s) where
return a = State $ \s -> (s,a)
>>=) = usualBind
(
usualBind :: forall s a b. State s a -> (a -> State s b) -> State s b
= State $ \s0 ->
usualBind ma k let (s1,a) = runState ma s0
in runState (k a) s1
ただし、State s
型の内容である関数s -> (s,a)
やreturn, (>>=)
といった関数は、
いずれも部分関数でないものだけを考えています。undefined
や再帰を用いて、
一部の入力に対して⊥
になるような定義は認めていません。1
この主張の意味するところには、勘違いしやすい箇所が2つあります。まず、
Monad
則を満たすインスタンスだけを数えている点です。
実は、Monad
則を満たさなくてもよいならば、幾らでも異なるインスタンスを作ることができます。
例えば、以下のインスタンスはコンパイルできますが、Monad
則を満たしません。
instance Monad (State s) where
return a = State $ \s -> (s, a)
>>=) = strangeBind
(
strangeBind :: forall s a b. State s a -> (a -> State s b) -> State s b
= State $ \s0 ->
strangeBind ma k let (s1, _) = runState ma s0
= runState ma s1
(s2, _) = runState ma s2
(_, a) in runState (k a) s1
2つ目に、「どんな型A
についても、Monad (State A)
のインスタンスは唯一である」
と混同しそうになる点があります。ここで主張していることは
- ⭕「『(任意の型
s
に対して使える)Monad (State s)
のインスタンス』は通常のStateモナドに限られる」
であって、
- ❌「任意の型
s
に対して『Monad (State s)
のインスタンスは通常のStateモナドに限られる』が成り立つ」
ではありません。
実際、後者は誤った主張です。
反例を挙げると、s
にBool
を代入した場合にそれは成り立ちません。
instance Monad (State Bool) where
-- 要 FlexibleInstances 拡張
......
と書けるインスタンスには、きちんとMonad
則を満たす”普通でない”インスタンスが多数存在します。
“普通でない”インスタンスの具体例
例えば、以下のようなMonad
インスタンスがあり、普通のStateモナドとは異なりますがMonad
則は満たしています。
toTriple :: State Bool a -> (Bool -> Bool, a, a)
State f) = (fst . f, snd (f False), snd (f True))
toTriple (
fromTriple :: (Bool -> Bool, a, a) -> State Bool a
= State $ \s ->
fromTriple (g, a0, a1) case s of
False -> (g s, a0)
True -> (g s, a1)
instance Monad (State Bool) where
return a = fromTriple (id, a, a)
>>= k =
ma let (f, a0, a1) = toTriple ma
= toTriple (k a0)
(g, b0, _) = toTriple (k a1)
(_, _, b1) in fromTriple (g . f, b0, b1)
更に、上記のMonad
インスタンスがMonad
則を満たすことを確かめると、
その際にBool -> Bool
がid
を単位元、flip (.)
を積とするモノイドを成していること
だけが重要であることに気づきます。
Bool -> Bool
と同じ数)の要素をもつ任意のモノイドを用いても
State Bool a
上に類似のMonad
インスタンスを定めることが可能です。
そのようなモノイドで同型でないものは35種類あり、それらが全て別々の”普通でない”Monad
インスタンス
を定めます。
証明の概略
証明は3つのパートに分けられます。
パート1: join
の変化だけ考えればよい
詳細は省きますが、parametricityというHaskellの言語としての性質から、
Monad (State s)
のインスタンスとして変化を付けられる可能性があるのは、
(>>=)
の部分だけであることがわかります。詳しく言うと、
任意のデータ型
F
に対してFunctor F
のインスタンスは唯一であるFunctor
則を満たす2つのfmap, fmap' :: forall a b. (a -> b) -> F a -> F b
に対して常にfmap f x === fmap' f x
が成り立つので、 “異なる”Functor
のインスタンスは作ることができない
Monad
則を考慮に入れずとも、return
は一つしか存在しない型
forall s a. a -> State s a
が付く式の値は、いずれもreturn :: forall s a. a -> State s a return a = State $ \s -> (s, a)
と同値である
が成り立ちます。したがって、「Monad
則を満たす(>>=)
としてあり得る値はいくつあるか」
が分かればよいのですが、ここで更に、証明での扱いやすさのため、(>>=)
の代わりに
joinを使うことにします。
元来join
は(>>=)
を使って定義されていますが、逆にjoin
から(>>=)
を復元することもできるので、
それぞれの型BindTy
とJoinTy
の間には全単射があります。
つまり、(>>=)
としてあり得る変化を調べる代わりに、join
としてあり得る変化を考えれば必要十分というわけです。
type BindTy = forall s a b. State s a -> (a -> State s b) -> State s b
type JoinTy = forall s a. State s (State s a) -> State s a
-- BindTy と JoinTy は同型
bindToJoin :: BindTy -> JoinTy
= \mma -> mma `bind'` id
bindToJoin bind'
joinToBind :: JoinTy -> BindTy
= \ma k -> join' (fmap k ma) joinToBind join'
例えば、どちらもBindTy
型の値であるusualBind
とstrangeBind
をそれぞれ
JoinTy
型に変換すると、以下のようになります。
-- usualJoin = bindToJoin usualBind
usualJoin :: JoinTy
= State $ \s0 ->
usualJoin mma let (s1, ma) = runState mma s0
in runState ma s1
-- strangeJoin = bindToJoin strangeBind
strangeJoin :: JoinTy
= State $ \s0 ->
strangeJoin mma let (s1, _) = runState mma s0
= runState mma s1
(s2, _) = runState mma s2
(_, ma) in runState ma s1
つまり、証明のゴールは、
JoinTy
型の値のうち、return
と組み合わせてMonad
則を満たすものは、 通常のStateモナドを定めるusualJoin
の1つに限られる
を示すことになります。
また、ここでいうMonad
則は、join
を用いて表現した以下の3つの等式のことです。
- 左単位則(left unit)
join . return === id
- 右単位則(right unit)
join . fmap return === id
- 結合則(associativity)
join . join === join . fmap join
パート2: ポリモーフィックな関数の代わりに代数的データ型で考えればよい
パート1で、証明のゴールを
JoinTy
型の値のうち、return
と組み合わせてMonad
則を満たすものは、 通常のStateモナドを定める1つだけである
に絞り込むことができました。しかし、直接この証明にとりかかるのはまだ早いです。
JoinTy
型というのは、型パラメータs, a
に関してforall
が付いた、ポリモーフィックな関数
type JoinTy = forall s a. State s (State s a) -> State s a
のことでした。そのようなポリモーフィックな関数すべてに対してMonad
則を満たすかどうかを調べていくというのは、
どうやればいいのでしょうか?
ここで、Boehm–Beraducciエンコーディングという、「代数的データ型をポリモーフィックな関数で表現する」 手法を使うことができます。 これは代数的データ型とポリモーフィックな関数が同型になるようなエンコーディングなので、逆に関数のほうを代数的データ型と見做すためにも使えます。
厳密な言明はここでは説明しませんが、Boehm–Beraducciエンコーディングは以下のような例をすべて包含する一般化になっています。2
自然数
Nat
↔︎forall r. (r -> r) -> r -> r
{-# LANGUAGE RankNTypes #-} -- 以下、他のすべての例にもRankNTypes拡張が必要 data Nat = Suc Nat | Z type NatEnc = forall r. (r -> r) -> r -> r -- encodeNat (Suc Z) = \s z -> s z -- encodeNat (Suc (Suc Z)) = \s z -> s (s z) encodeNat :: Nat -> NatEnc Suc n) = \s z -> s (encodeNat n s z) encodeNat (Z = \_ z -> z encodeNat decodeNat :: NatEnc -> Nat = f Suc Z decodeNat f
Bool
↔︎forall r. r -> r -> r
data Bool = False | True type BoolEnc = forall r. r -> r -> r encodeBool :: Bool -> BoolEnc False = \x _ -> x encodeBool True = \_ y -> y encodeBool decodeBool :: BoolEnc -> Bool = f False True decodeBool f
リスト
List a
↔︎forall r. (a -> r -> r) -> r -> r
data List a = Cons a (List a) | Nil type ListEnc a = forall r. (a -> r -> r) -> r -> r -- encodeList (Cons a0 Nil) = \c n -> c a0 n -- encodeList (Cons a0 (Cons a1 Nil)) = \c n -> c a0 (c a1 n) encodeList :: List a -> ListEnc a Cons a as) = \c n -> c a (encodeList as c n) encodeList (Nil = \_ n -> n encodeList decodeList :: ListEnc a -> List a = f Suc Z decodeList f
二分木
BinTree a
↔︎forall r. (a -> r) -> (r -> r -> r) -> r
data BinTree a = Tip a | Branch (BinTree a) (BinTree a) type BinTreeEnc a = forall r. (a -> r) -> (r -> r -> r) -> r encodeBT :: BinTree a -> BinTreeEnc a Tip x) = \t _ -> t x encodeBT (Branch l r) = \t b -> b (encodeBT l t b) (encodeBT r t b) encodeBT ( decodeBT :: BinTreeEnc -> BinTree a = f Tip Branch decodeBT f
型JoinTy
を、Boehm–Beraducciエンコーディングが適用できる形になるまで同型な型に変形させていくと、
以下のようになります。
JoinTy
= forall s a. State s (State s a) -> State s a
-- Stateのnewtypeを剥がす
~ forall s a. (s -> (s, s -> (s, a))) -> (s -> (s, a))
-- (x ->)はタプルに分配できる:
-- (x -> (y,z)) ~ (x -> y, x -> z)
~ forall s a. (s -> s, s -> s -> (s, a)) -> s -> (s, a)
~ forall s a. (s -> s, (s -> s -> s, s -> s -> a)) -> s -> (s, a)
-- カリー化:
-- ((x, y) -> z) ~ (x -> y -> z)
~ forall s a. (s -> s) -> (s -> s -> s) -> (s -> s -> a) -> s -> (s, a)
-- 引数の順序を入れ替え
~ forall s a. (s -> s) -> (s -> s -> s) -> s -> (s -> s -> a) -> (s, a)
-- 引数の型が a に依存しない関数と forall a. を交換
~ forall s. (s -> s) -> (s -> s -> s) -> s -> forall a. (s -> s -> a) -> (s, a)
-- カリー化
~ forall s. (s -> s) -> (s -> s -> s) -> s -> forall a. ((s,s) -> a) -> (s, a)
-- Yoneda
~ forall s. (s -> s) -> (s -> s -> s) -> s -> Yoneda ((,) s) (s,s)
~ forall s. (s -> s) -> (s -> s -> s) -> s -> (s,(s,s))
-- (s,(s,s)) ~ (s,s,s)
-- (x ->)をタプルに分配
~ forall s. (
-> s) -> (s -> s -> s) -> s -> s,
(s -> s) -> (s -> s -> s) -> s -> s,
(s -> s) -> (s -> s -> s) -> s -> s
(s
)-- forall をタプルに分配:
-- forall x. (f x, g x) ~ (forall x. f x, forall x. g x)
~ (
forall s. (s -> s) -> (s -> s -> s) -> s -> s,
forall s. (s -> s) -> (s -> s -> s) -> s -> s,
forall s. (s -> s) -> (s -> s -> s) -> s -> s
)-- 各成分の型を (TEnc := forall s. (s -> s) -> (s -> s -> s) -> s -> s) とおく
= (TEnc, TEnc, TEnc)
ここで出てきたTEnc = forall s. (s -> s) -> (s -> s -> s) -> s -> s
という型は、
代数的データ型T
にBoehm–Beraducciエンコーディングを適用した結果になっています。
data T = F T | G T T | X
-- TEnc は T の Boehm--Beraducciエンコーディング
type TEnc = forall s. (s -> s) -> (s -> s -> s) -> s -> s
encodeT :: T -> TEnc
F t) = \f g x -> f (encodeT t f g x)
encodeT (G t u) = \f g x -> g (encodeT t f g x) (encodeT u f g x)
encodeT (X = \_ _ x -> x
encodeT
decodeT :: TEnc -> T
= enc F G X decodeT enc
したがって、JoinTy
は、ある全単射encodeJoin, decodeJoin
によって、
代数的データ型であるT
の3つ組(T,T,T)
と同型です。
これらの同型を実際にHaskellで書くと以下の通りになります。
encodeJoin :: (T,T,T) -> JoinTy
= State $ \s0 ->
encodeJoin (t,l,r) mma let s2 = encodeT t f g s0
= encodeT l f g s0
sLeft = encodeT r f g s0
sRight in (s2, h sLeft sRight)
where
=
f s0 let (s1,_) = runState mma s0
in s1
=
g s0 s1 let (_,ma) = runState mma s0
= runState mma s1
(s2,_) in s2
=
h s0 s1 let (_,ma) = runState mma s0
= runState ma s1
(_,a) in a
decodeJoin :: JoinTy -> (T,T,T)
= combine $ runState (join' mmT) X
decodeJoin join' where
= (t,l,r)
combine (t,(l,r)) mmT :: State T (State T (T,T))
= State $ \s -> (F s, State $ \s' -> (G s s', (s,s'))) mmT
例えば、通常のStateモナドを定めるusualJoin
や、Monad
則を満たさない例として挙げたstrangeJoin
をエンコードすると、
以下のようになります。
-- usualDef = decodeJoin usualJoin
usualDef :: (T,T,T)
= (G X (F X), X, F X)
usualDef
strangeDef :: (T,T,T)
= (G (F (F X)) (F X),F (F X),F X) strangeDef
(T,T,T)
のような代数的データ型であれば、それがとる値すべてに対する性質を証明していくことはできそうですね!
示すべき証明のゴールは
join = encodeJoin def
がMonad
則を満たすようなdef :: (T,T,T)
はusualDef
のみである
になりました。 join = encodeJoin def
を代入したMonad
則は以下の通りです。
- 左単位則(left unit)
encodeJoin def . return === id
- 右単位則(right unit)
encodeJoin def . fmap return === id
- 結合則(associativity)
encodeJoin def . encodeJoin def === encodeJoin def . fmap encodeJoin def
そして更に、これらのMonad
則において、
- (左|右)単位則の両辺は
forall s a. State s a -> State s a
という型 - 結合則の両辺は
forall s a. State s (State s (State s a)) -> State s a
という型
です。JoinTy = forall s a. State s (State s a) -> State s a
に対して行ったのと同様にして、
これらの型も適切な代数的データ型と同型になっています。
type UnitLawTy = forall s a. State s a -> State s a
-- UnitLawTy ~ (Nat, Nat)
encode1 :: (Nat, Nat) -> UnitLawTy
ma :: State s a) =
encode1 (n,m) (let f = fst . runState ma :: s -> s
= snd . runState ma :: s -> a
ret in State $ \s0 -> (encodeNat n f s0, ret (encodeNat m f s0))
decode1 :: UnitLawTy -> (Nat, Nat)
=
decode1 mm let mNat = State Nat Nat
= State $ \n -> (Suc n, n)
mNat in runState (mm mNat) Z
type AssocLawTy = forall s a. State s (State s (State s a)) -> State s a
data S = Leaf | A S | B S S | C S S S
encode3 :: (S, S, S, S) -> AssocLawTy
= {- 省略 -}
encode3 decode3 :: AssocLawTy -> (S, S, S, S)
= {- 省略 -} decode3
これらのエンコーディングは同型写像なので、等式の両辺に適用しても 等式が成り立つ条件を変えません。したがって、以下のような補助関数を用いて…
idRep :: (Nat, Nat)
= decode1 (id :: forall s a. State s a -> State s a)
idRep
leftUnitLHS :: (T,T,T) -> (Nat, Nat)
= decode1 (encodeJoin def . return)
leftUnitLHS def
rightUnitLHS :: (T,T,T) -> (Nat, Nat)
= decode1 (encodeJoin def . fmap return)
rightUnitLHS def
assocRHS :: (T,T,T) -> (S, S, S, S)
assocLHS,= decode3 (encodeJoin def . encodeJoin def)
assocLHS def = decode3 (encodeJoin def . fmap (encodeJoin def)) assocRHS def
…Monad
則は以下のように記述できます。
- 左単位則
leftUnitLHS def = idRep
- 右単位則
rightUnitLHS def = idRep
- 結合則
assocLHS def = assocRHS def
この等式は代数的データ型の値の間の等式なので、変数def
の動く範囲も含めて
「ポリモーフィックな関数」に一度も言及せずにMonad
則を表すことができたことになります。
パート3: 場合分けを頑張れば答えが出せる
さて、前パートのleftUnitLHS
などの補助関数は、型を見るとわかるように
代数的データ型から代数的データ型へのごく普通の関数であり、
その計算方法もencode*, decode*
などを展開すると具体的にわかります。
例えば、idRep
を実際に評価してみれば3idRep = (Suc Z, Z)
がわかりますし、leftUnitLHS, rightUnitLHS
も、
具体的には以下のような関数(に同値な振る舞いの関数)であることが計算できます。
leftUnitLHS (t,l,r)= (countGs t, countGs r)
rightUnitLHS (t,l,r)= (countFs t, countFs l)
-- ただしcountGs, countFsは以下の関数
countGs :: T -> Nat
F t') = countGs t'
countGs (G _ t') = Suc (countGs t')
countGs (X = Z
countGs
countFs :: T -> Nat
F t') = Suc (countFs t')
countFs (G _ t') = countFs t'
countFs (X = Z countFs
これにより、Monad
則は、具体的な定義をもつ補助関数たちによって書かれた、
代数的データ型(T,T,T)
上を動く変数def
についての”連立方程式”と見做すことができます。
後はひたすら場合分けをしていくことでこの”連立方程式”を解き、
その解がdef = usualDef = (G X (F X), X, F X)
の一点に限ることを示すのみです。
本記事にその細部までは書ききれませんが、雰囲気を感じていただくため、
単位則だけからわかる部分までを解説します。
上に結果だけ記したleftUnitLHS, rightUnitLHS
の計算方法をMonad
則に代入すると、
- 左単位則
leftUnitLHS (t,l,r) = (countGs t, countGs r) = (Suc Z, Z)
- 右単位則
rightUnitLHS (t,l,r) = (countFs t, countFs l) = (Suc Z, Z)
となります。タプル間の等式を成分ごとに書けば、(eq1)–(eq4)が得られます。
= Suc Z -- (eq1)
countGs t = Z -- (eq2)
countGs r = Suc Z -- (eq3)
countFs t = Z -- (eq4) countFs l
これから、以下の3式が成り立ちます。
ある自然数
n
を用いて、r = F^n X
(eq2)より
countGs r = Z
ですが、これはr
がG _ _
という部分項を含まないこと、すなわち= X | F X | F (F X) | ... r
であることがわかります。
ある自然数
m
を用いて、l = (G _)^m X
(eq4)より
countFs l = Z
です。これは= X | G u₁ X | G u₁ (G u₂ X) | ... l
を意味します。正確な情報を忘れてしまってよいのであれば、
l = (G _)^m X
と表記してもよいでしょう。
ある
u :: T
を用いて、t = F (G u X)
またはt = G u (F X)
(eq1)と(eq3)より、
countFs t = countGs t = Suc Z
です。 ここでT
型の値t
の値について場合分けをします。t = X
ではありえません。t = F t'
の場合:まず、
countFs
の定義よりcountFs (F t') = Suc (countFs t') = Suc Z
であり、countFs t' = Z
でなければなりません。(eq4)と同様にしてt' = (G _)^m X
がわかります。また、
countGs (F t') = countGs t' = countGs ((G _)^m X) = Suc Z
であるので、m = 1
が得られます。すなわち、t'
はあるu
を用いてt' = G u X
と表されます。t = G u t'
の場合:countGs
の定義よりcountGs (G u t') = Suc (countGs t') = Suc Z
であり、countGs t' = Z
でなければなりません。(eq2)と同様にしてt' = F^n X
がわかります。また、
countFs (G u t') = countFs t' = countFs (F^n X) t = Suc Z
であるので、n = 1
が得られます。すなわち、t' = F X
です。
場合分けをまとめると、ある
u
を使ってt = F (G u X)
またはt = G u (F X)
のどちらかであることがわかります。
通常のStateモナドはMonad
則を満たしているので、
それに対応するusualDef = (G X (F X), X, F X)
も当然にこれらの条件をすべてクリアしているはずです。
確かめてみてください。
まとめ
Haskellにおいてinstance Monad (State s) where ...
と書けるインスタンスでMonad
則を満たすものは、
“普通の”Stateモナドしか存在しません。この記事ではその証明の大まかな方針を説明しました。
実は、上記の形のインスタンスを持つReverse State Monadというものがあります。 Reverse State Monadは、通常のStateモナドとは異なり、遅延評価と再帰を使って
(>>=)
が定義されるモナドで、一定の条件下に限ればMonad
則を満たしますが、 その条件を満たさなかった場合(>>=)
が「全域でない関数s -> (s, a)
」を作り出すなど、いまひとつ同じ土俵に乗りません。私の知識不足が大きいでしょうが、このモナドは後述する Boehm–Beraducci エンコーディングの枠組みで捉えることもできず、 どう考えるべきかはっきりしません。なるべくad hocに見えない方法でReverse State Monadの類を除外する為に “部分関数はないものとする”という前提を設けました。↩︎
チャーチ・エンコーディング をご存知の場合、これは”厳密な型の付いた”チャーチ・エンコーディングと考えてもかまいません。↩︎
GHCiなどを使って
idRep
を評価させることができます↩︎