二重否定モナド
本記事では二重否定モナドを発見し、その正体を探ります。
なお、例外の発生や無限ループなどは無いものとします。
命題としての型
命題P
に対してその証拠e
を考え、e :: P
と書くことにします。
「e
は型P
の要素である」とも言います。
命題P
が少なくとも1つ以上の要素を持つとき、その命題は成り立つと言います。
正当化
命題A
が成立することの証拠a
と、命題A → B
(A
ならばB
)が成立することの証拠f
を持っているとします。
このとき命題B
が成立することの証拠も得られます。その証拠をf a
と書くことにします。
ところで、命題A
やB
を型としてみなし、命題A → B
を関数の型A -> B
と見なしましょう。
すると、f :: A -> B
はただの関数で、f a
は関数適用に見えてきます。
命題A
に対し、「A
が成り立つならばA
が成り立つ」という恒真命題は恒等関数id
に対応します。
同様に、命題A → B
の証拠f
と命題B → C
の証拠g
があるとき、命題A → C
の証拠は関数合成g . f
で表すことができます。
命題と型の対応関係は他にもありますが割愛します。
以降、型と命題を同一視します。
矛盾の型
Data.Void
で定義されているVoid
は要素を持たない型です。
Void
を返すような関数f :: a -> Void
を書いてみます。
f :: a -> Void
f x = ???
???
部分には何を書けば良いでしょう?
そもそも、Void
には要素が1つも無いのでこのような関数は書くことができないように思われます。
もし、Void
を返すような関数が書けたとすれば、Void
が値を持たないという事実に矛盾してしまうからです。
唯一例外があり、a
がVoid
のときは関数を書くことができます。
f :: Void -> Void
f x = x
「Void
の要素x
を持っている」というあり得ない仮定から、Void
の要素を返すことに成功したわけです。
あり得ない仮定からはどんな結論をも導くことができ、Haskellではabsurd
という関数が標準で用意されています。(空虚な真)
absurd :: Void -> a
否定
関数P -> Void
が得られたとします。
ここでP
の要素を持っていると仮定すると、関数適用によりVoid
の要素が得られることになります。これは矛盾です。
先ほどの観察と合わせて、P
は値を持たないはずです。
つまり、P
は成り立ちません。
したがって、命題P
の否定「P
でない」はP → Void
で表すことができます。
「P
でない」という命題を考える際は、P
の成立を仮定してVoid
を導くような関数の型を考えれば良いわけです。
二重否定
命題P
の二重否定「(P
でない)でない」も同様に表すことができます。
(P → Void) → Void
二重否定の除去?
古典的な数学では、二重否定は即座に除去することができます。
つまり、「(P
でない)でない」ことを証明すれば、ただちに「P
が成り立つ」ことを証明したことになります。
しかし、今はHaskellで記述しているので事情が少し異なります。
この文脈では、「P
が成り立つ」という主張は単に"それが真であることを証明した"ということだけに留まらず、"それが真であることを示すアルゴリズムを記述した"ということになります。
実際、一般にP
の二重否定の成立を示すアルゴリズムを持っているからと言って、P
の成立を示すアルゴリズムが得られるとは限らないようです。
二重否定モナド
二重否定を定義し、モナドインスタンスにしていきます。
newtype DoubleNegation a = DoubleNegation ((a -> Void) -> Void)
先へ進む前にnewtype
で作った型コンストラクタを剥がすヘルパー関数を用意しておきます。
unwrap :: DoubleNegation a -> (a -> Void) -> Void
unwrap (DoubleNegation notNotA) = notNotA
これはただのヘルパー関数ですが、読み方を変えると「a
の二重否定」が成り立つ証拠と「a
の否定」が成り立つ証拠の両方を持っているような状況は矛盾であるとも解釈できますね。
ファンクター
「a
ならばb
」が成り立つ証拠と「a
の二重否定」が成り立つ証拠を持っているとき、「b
の二重否定」も成り立ちます。
やってみましょう。
instance Functor DoubleNegation where
fmap aImpliesB (DoubleNegation notNotA) = DoubleNegation notNotB
where notNotB notB = notNotA (notB . aImpliesB)
この主張の面白いところは、a
自体の成立を示すアルゴリズムを持っていなくとも、a
の二重否定の成立を示すアルゴリズムがあれば、a
ならばb
を用いてb
の二重否定の成立を示せるという点にあります。
関手則を満たすことも確認しておきましょう。
関手則
fmap id (DoubleNegation notNotA)
= DoubleNegation (\notB -> notNotA (notB . id))
= DoubleNegation (\notB -> notNotA notB)
= DoubleNegation notNotA
= id (DoubleNegation notNotA)
fmap (g . f) (DoubleNegation notNotA)
= DoubleNegation (\notB -> notNotA (notB . (g . f)))
= DoubleNegation (\notB -> notNotA ((notB . g) . f))
(fmap g . fmap f) (DoubleNegation notNotA)
= fmap g $ fmap f (DoubleNegation notNotA)
= fmap g $ DoubleNegation (\notB' -> notNotA (notB' . f))
= DoubleNegation (\notB' -> (\notB -> notNotA (notB' . f)) (notB . g))
= DoubleNegation (\notB -> notNotA ((notB . g) . f))
アプリカティブファンクター
まずはpure
に関して考えましょう。
a
が成り立つことの証拠を持っていれば、a
の二重否定の成立も示せるということですね。
instance Applicative DoubleNegation where
pure = DoubleNegation . aImpliesNotNotA
where aImpliesNotNotA a notA = notA a
aImpliesNotNotA
関数はData.Function
に定義されている(&)
と同じです。
先ほど、二重否定の成立から元の命題を示せるとは限らないと言ったばかりですが、その逆は常に成り立つことが分かります。
続いてap
についても考えましょう。
(a
ならばb
)の二重否定が成立することの証拠と、a
の二重否定が成立することの証拠を持っているならばb
の二重否定が成立を示せるということですね。
instance Applicative DoubleNegation where
(DoubleNegation notNotAImpliesB) <*> (DoubleNegation notNotA) =
DoubleNegation notNotB
where
notNotB notB = notNotAImpliesB notAImpliesB
where
notAImpliesB aImpliesB = notNotA $ notB . aImpliesB
繰り返しになりますが、a
自体やa
ならばb
自体を成立させるアルゴリズムに言及していないところが面白い点です。
アプリカティブ関手則の成立の確認は割愛します。
補題の証明
モナドの実装の前に補題をいくつか示します。
いつでも二重否定を外せるとは限りませんが、特殊なケースでは二重否定を外すことができます。
偽である命題の二重否定は外すことができます。
doubleNegationOfFalseImpliesFalse :: DoubleNegation Void -> Void
doubleNegationOfFalseImpliesFalse (DoubleNegation notNotFalse) = notNotFalse id
否定の命題の二重否定から元となる否定の命題を導けます。
doubleNegationOfNegationImpliesNegation :: DoubleNegation (a -> Void) -> a -> Void
doubleNegationOfNegationImpliesNegation (DoubleNegation notNotNotA) a =
notNotNotA (\notA -> notA a)
この時点でモナドの片鱗が見えるかと思います。
モナド
最後にbind
について考えます。
a
の二重否定が成立することの証拠と、a
が成り立つならばb
の二重否定が成り立つことの証拠を持っているときに、b
の二重否定の成立を示せるということですね。
instance Monad DoubleNegation where
doubleNegationA >>= aImpliesDoubleNegationB =
DoubleNegation
$ doubleNegationOfNegationImpliesNegation
$ fmap (unwrap . aImpliesDoubleNegationB) doubleNegationA
モナド則の確認は……割愛します。
継続モナド
冷静になって型を眺めてみると、二重否定モナドは単なる継続モナドの一具体例に過ぎません。
mtl
パッケージのCont
モナドを拝借しましょう。
newtype Cont r a = Cont { runCont :: (a -> r) -> r }
ここでr
をVoid
とすれば先ほどまで扱っていたa
の二重否定に早変わりです。
type DoubleNegation a = Cont Void a -- Cont { runCont :: (a -> Void) -> Void }
試しに「否定の命題の二重否定から元となる否定の命題を導ける」ことを示してみます。
module Data.DN where
import Control.Monad.Cont (Cont, runCont)
import Data.Void (Void)
type DoubleNegation a = Cont Void a
doubleNegationOfNegationImpliesNegation' :: DoubleNegation (a -> Void) -> a -> Void
doubleNegationOfNegationImpliesNegation' doubleNegationOfNegationA a =
runCont ((\notA -> notA a) <$> doubleNegationOfNegationA) id
まとめ
二重否定がモナドになることが分かりました。
しかしその正体は単なる継続モナドでした。
以下、二重否定モナドの自力実装のまとめです。
module Data.DoubleNegation where
import Data.Void (Void)
newtype DoubleNegation a = DoubleNegation ((a -> Void) -> Void)
unwrap :: DoubleNegation a -> (a -> Void) -> Void
unwrap (DoubleNegation notNotA) = notNotA
instance Functor DoubleNegation where
fmap aImpliesB (DoubleNegation notNotA) = DoubleNegation notNotB
where
notNotB notB = notNotA (notB . aImpliesB)
instance Applicative DoubleNegation where
pure = DoubleNegation . aImpliesNotNotA
where aImpliesNotNotA a notA = notA a
(DoubleNegation notNotAImpliesB) <*> (DoubleNegation notNotA) =
DoubleNegation notNotB
where
notNotB notB = notNotAImpliesB notAImpliesB
where
notAImpliesB aImpliesB = notNotA $ notB . aImpliesB
doubleNegationOfFalseImpliesFalse :: DoubleNegation Void -> Void
doubleNegationOfFalseImpliesFalse (DoubleNegation notNotFalse) = notNotFalse id
doubleNegationOfNegationImpliesNegation :: DoubleNegation (a -> Void) -> a -> Void
doubleNegationOfNegationImpliesNegation (DoubleNegation notNotNotA) a =
notNotNotA (\notA -> notA a)
instance Monad DoubleNegation where
doubleNegationA >>= aImpliesDoubleNegationB =
DoubleNegation
$ doubleNegationOfNegationImpliesNegation
$ fmap (unwrap . aImpliesDoubleNegationB) doubleNegationA
Discussion