Scalaで論理体系を作る話 - 2. 命題論理その2

直観主義論理はカリーハワード対応に見て取れる通り、型付きラムダ計算ほどの単純なモデルが表現できるシンプルな論理ですが、制約も多いのでした。この記事は、より「強い」、つまりより強力な論理である古典論理をどうScalaプログラム内で構築できるのか、について記述します。

[Scalaで論理体系を作る話 - 1. 命題論理その1](https://qiita.com/Kory__3/items/96efec755f4045b95518)

その前に…

# 補足 - 証明における禁忌

Scalaプログラム上の証明は、書き方によっては意味を為さなくなります。そのような二つのケースとして、**例外を投げる**ことと**自分自身を証明に使ってしまう(循環論法)**ことの二つが挙げられます。

論理式として、$A$を考えます。これはどのような型Aにも値が存在するという主張であり、本来ならば証明はできません。

例外を投げるというのは、例えば

```Scala
def incompleteProof[A]: A = throw new NotImplementedError()
```

といったものを指します。`throw`式は型 `Nothing` を持つため、結果が`throw`式になっていると証明を放棄しているにもかかわらずコンパイルが通ってしまいます。「証明を後に回す」用途で`???`を置くことはありますが、証明が完了したときには`throw`式が関数定義内に現れてはいけません。

循環論法は、自分自身を右辺にて呼び出すような関数に対応します。例えば、

```Scala
def selfReferentialProof[A]: A = selfReferentialProof[A]
```

といったものを指します。また、相互再帰(二つの関数がそれぞれを呼び出す)であってもなりません。
証明内では、すでに実装された関数のみ呼び出すことが許されます。

### 証明の意味論

直観主義論理の範囲では、証明が上に説明されているような性質を満たすように記述されている場合、関数呼び出しを行った時に例外を投げず必ず停止します(単純型付きラムダ計算の停止性)。正しい証明で評価されない箇所に再帰呼び出しを仕込めるため、逆は真ではありません。

しかし、後述するように、古典論理の体系を考えると正しい証明であっても必ずしもそのような良い性質を持つとは限りません。古典論理のScala上での表現は、どちらかというと純粋に形式的なものと捉えるのが楽であると思います(継続渡しスタイルなどが深く関わっているようですが、理解しきれていませんので疑問として書いておきます)。

> 疑問：古典論理の証明を表現するコードの妥当性に単純な意味論はあるか？

# 本題

[前回の記事](https://qiita.com/Kory__3/items/96efec755f4045b95518#%E3%82%AB%E3%83%AA%E3%83%BC%E3%83%8F%E3%83%AF%E3%83%BC%E3%83%89%E5%AF%BE%E5%BF%9C---%E5%9E%8B%E3%81%A8%E5%91%BD%E9%A1%8C%E3%81%AE%E5%AF%BE%E5%BF%9C)にて、否定演算子`￢`が、`type ￢[A] = A => Nothing`のように表されると書きました。また、公理系LIPは公理系LPの二重否定除去則$\neg \neg A \to A$をより弱い否定除去により置き換えていたのでした。つまり、プログラム内で二重否定除去を表現できれば古典論理が表現できたことになります。

二重否定除去をコードで書くと以下のようなシグネチャになります。

```Scala
def removeDoubleNegation[A]: ((A => Nothing) => Nothing) => A = ???
```

これは、「「`A`を渡すと例外が飛ぶ関数」を受け取ると例外が飛ぶ関数」から`A`の関数と捉えることができます。本当にこのようなものが実装できるのでしょうか？

## 公理化か実装か

プログラム上で実装"できそうにない"ような形式体系を考える場合、`trait`に定義したメソッドを公理として持ちまわす、ということができます。例えば、

```Scala
trait ClassicalLogic {
  def removeDoubleNegation[A]: ((A => Nothing) => Nothing) => A
}
```

として、古典論理に依存する体系をすべて`ClassicalLogic`の実装に依存するようにすれば、形式的に証明を行っていくことができます。ここで、`ClassicalLogic`の実装が真に存在しない場合、体系の意味論的解釈をプログラム上で考えることができなくなります。

しかし、Scalaにはこれを表現する方法が存在します。ですので、`trait`ではなく実装を持つ`object`に二重否定除去を書くことができます。

## 広域脱出による二重否定除去
JavaやScalaは、例外が`throw`されると、`catch`節に捕捉されるまでスタックを一気に抜け続けます。
そこで、「評価されたら例外を投げる」ような関数を引数に渡すことで、「関数が評価されたら瞬時に呼び出し元に戻る」ようなものが表現できます。

### Scalazでの実装
抽象構造を提供するライブラリ、[Scalaz](https://github.com/scalaz/scalaz)に、大域脱出により二重否定除去を表現する[コードがあります](https://github.com/scalaz/scalaz/blob/series/7.3.x/core/src/main/scala/scalaz/Forall.scala#L22-L31)ので、改変したものを紹介[^1]します。

```Scala
def removeDoubleNegation[A](f: ((A => Nothing) => Nothing)): A = {
  // 制御用の例外のクラス
  case class Control(arg: (A => Nothing) => Nothing) extends Throwable(null, null, true, false)

  try {
    f(arg: A => throw new Control(arg))
  } catch {
    case Control(arg) => arg
  }
}
```

`f`は返り値の型が`Nothing`であるので、引数を評価するか、評価せずに`Control`以外の例外を投げるかのどちらかです。

`Control`以外の例外が投げられた場合は、`catch`をすり抜けて外部に例外を吐き出します。この時、`try`式の評価値の型は`Nothing`であるため、型検査に通ります。

そうではなく`f`の引数が引数`arg`にて評価された場合は、即座に`Control(arg)`が`removeDoubleNegation`の`try`節まで投げられ、`catch`にて捕捉されます(`Control`は`removeDoubleNegation`でしか定義されないので、他の箇所で捕捉されることが無いことに注意してください)。この時、実体`arg: A`が例外とともに呼び出し元に戻されるため、`arg`を返して終わりです。

> 疑問：
> 「例外を勝手に投げない」というルールを守っている場合は、最初のケースになることはない。
> しかし、後述するようにコンパイルに通るコードが例外を投げる場合がある。なぜ？

### 糖衣構文

実は先ほどのコードはたった一行で表現できます。

```Scala
def removeDoubleNegation[A](f: ((A => Nothing) => Nothing)): A = f(a => return a)
```

xuweiさんに詳細を教えていただきました。
<blockquote class="twitter-tweet" data-partner="tweetdeck"><p lang="ja" dir="ltr">Javaと違って、ラムダを終わりとするreturnではなく、そこから一番内側のメソッドの終わりになります。<br>(なお、そういうパターンの場合、内部実装としては例外で実装されます)</p>&mdash; Kenji Yoshida (@xuwei_k) <a href="https://twitter.com/xuwei_k/status/1017049797830721536?ref_src=twsrc%5Etfw">July 11, 2018</a></blockquote><blockquote class="twitter-tweet" data-partner="tweetdeck"><p lang="ja" dir="ltr">はい。<br>-Xprint:jvm<br>という、途中の抽象構文木をコンパイル時に表示してくれる特殊なコンパイルオプションつけるとこんな感じに<a href="https://t.co/O2gU8rLGFk">https://t.co/O2gU8rLGFk</a></p>&mdash; Kenji Yoshida (@xuwei_k) <a href="https://twitter.com/xuwei_k/status/1017054756022333445?ref_src=twsrc%5Etfw">July 11, 2018</a></blockquote>

先ほどのコードと全く同じように展開されることがわかります。

## True or Falseがなぜわかる？

直観主義論理で証明できず、古典論理で証明できる有名な例の一つとして_排中律_が挙げられます。Twitterで「XXである場合とそうでない場合があるぞい」と主張している排中律博士Botを見かけたことがあるかもしれません。

<blockquote class="twitter-tweet" data-partner="tweetdeck"><p lang="ja" dir="ltr">つまり何が言いたいかっていうとラーメンは神である場合と神でない場合があるぞい</p>&mdash; 排中律博士bot (@dr_haichu_bot) <a href="https://twitter.com/dr_haichu_bot/status/1083352019203645440?ref_src=twsrc%5Etfw">January 10, 2019</a></blockquote>

排中律とは、どんな命題Aに対しても、Aが真であるか、Aが偽であるかのどちらかであることを主張します。
論理式で書くと、

```math
A \lor \neg A
```

を主張します。

これは"真偽値"が意味論になる古典論理においては最も自明な定理のうちの一つなのですが、カリーハワード対応との相性はよいものとは言えません。コードでこの主張を記述すると、

```Scala
def middleExcl[A]: Either[A, A => Nothing] = ???
```

となります。実際、二重否定除去`removeDoubleNegation`が与えられていると、次のような[実装](https://github.com/kory33/scala-proofs/blob/master/src/main/scala/com/github/kory33/proof/logic/propositional/ClassicalLogicSystem.scala#L17-L31)が存在します。

```Scala
def middleExclusion[A]: Either[A, A => Nothing] = {
  val negNegProp: (Either[A, A => Nothing] => Nothing) => Nothing = { negProp =>
    val negNegA: (A => Nothing) => Nothing = { notA =>
      val prop: Either[A, A => Nothing] = Right(notA)
      negProp(prop)
    }
    val a: A = removeDoubleNegation(negNegA)
    val prop: Either[A, A => Nothing] = Left(a)
    negProp(prop)
  }
  removeDoubleNegation(negNegProp)
}
```

このコードは再帰も例外を投げるコードも表面的には含まれていませんが、先ほど言及した「直観主義論理の証明の良い性質」が崩れていることがわかります。

`A`に恒偽の型、例えば`Nothing`を入れることを考えましょう。すると、`Nothing`または`Nothing => Nothing`のどちらかが入った`Either`が戻されるはずです。前者を評価すると例外が飛ぶので、例外が飛んでほしくなければ後者でなくてはなりません。

今度は`A`に恒真の型、例えば`Unit`を入れることを考えましょう。ジェネリックな0引数関数は、型パラメータに依らず同じ値が返ってくるはずです。つまり、例外が飛んでほしくなければ`Unit => Nothing`が返ってきます。ここで、この戻り値にUnitを適用すると、例外が飛びます。

### 排中律は不自然？

カリーハワード対応によると、型の値の存在が定理と対応するのでした。すなわち、`middleExcl`が証明できるということは、**どのような型Aに対しても**、`A`か`A => Nothing`を返すような実装が存在するということです。これはプログラムの上ではかなりおかしな話であり、例外を発生させないようにするためには「できるだけ」Aを返すべきですが、値が存在しない場合は後者を返さなくてはなりません。しかし、当然コンパイラに事前に値の在住性を知る余地などありません。

型チェッカを納得させるような実装ができるとはいえ、排中律はこのような形式化の中では不自然な結果であると言えると思います[^2]。

# 述語論理へ

次の記事では、述語論理のScala上での表現を解説します。述語論理の表現はできているもののなかなか難しく、非常に実験的な内容になるとは思います。

[^1]: Copyright (c) 2009-2018 Tony Morris, Runar Bjarnason, Tom Adams, Kristian Domagala, Brad Clow, Ricky Clarkson, Paul Chiusano, Trygve Laugstøl, Nick Partridge, Jason Zaugg, Sam Halliday All rights reserved.
[^2]: ホンマか？