❤️‍🔥

【Idris】自然数の型を定義して「∀n∈N. n+0=n」を証明する

2024/07/15に公開

自然数の型を定義しよう

Idrisには自然数の型である Nat 型が存在しますが、標準の Nat 型を用いずにデータ型 MyNat を自作してみましょう。定義は以下の通りです。

MyNat.idr
data MyNat = Zero | Succ MyNat

MyNatZero または Succ MyNat のいずれかの値をとります。 MyNat の定義に MyNat が使用されていますね。値が再帰的に定義されており、以下のように構成されていきます。

data MyNat = Zero | Succ MyNat
           = Zero | Succ Zero | Succ (Succ MyNat)
           = Zero | Succ Zero | Succ (Succ Zero) | Succ (Succ (Succ MyNat))
           = .....

確かに 0, 1, 2, ... と対応していることが分かりますね!

自然数の足し算を定義しよう

自然数の足し算を計算する関数 mynat_plus を定義します。 MyNat 型の引数を2つ受け取り MyNat 型の値を返すので、型シグネチャは次のようになります。

MyNat.idr
total mynat_plus : MyNat -> MyNat -> MyNat

第一引数の値で場合分けしましょう。第一引数が Zero の場合は第二引数をそのまま返します。

MyNat.idr
total mynat_plus : MyNat -> MyNat -> MyNat
mynat_plus Zero y = y

第一引数が Succ x (1以上の自然数)の場合は再帰的に定義します。

MyNat.idr
total mynat_plus : MyNat -> MyNat -> MyNat
mynat_plus Zero y = y
mynat_plus (Succ x) y = Succ (mynat_plus x y)

(x+1)+y=1+(x+y)という変形ですが、 mynat_plus の引数に着目したときに、第一引数が1つ小さくなっていることが分かります。

第一引数が1つ小さい mynat_plus が再帰的に呼び出されることと、 mynat_plus Zero y が定義されていることから、関数 mynat_plus はいずれ停止することが保証されます。

それでは、関数 mynat_plus を実際に動かしてみましょう。

$ idris2 MyNat.idr
Main> mynat_plus (Succ Zero) (Succ Zero)
Succ (Succ Zero)

1+1=2 となりましたね!!嬉しい!

よく「1+1=2を証明して?」みたいなこと言われますよね。

mynat_plus (Succ Zero) (Succ Zero)
= Succ (mynat_plus Zero (Succ Zero))
= Succ (Succ Zero)      Q.E.D.

この計算仮定を示してやりましょう!!

0+n=nを証明しよう

ん?タイトルの n+0=0 の証明とは異なるのでしょうか?そうです、(今回の mynat_plus の定義では)若干異なります。実際にやってみながら確認していきましょう。命題:「すべての自然数 n に対して 0+n=n である。」を型で表現すると次のようになります。

MyNat.idr
total first_proof : (n : MyNat) -> mynat_plus Zero n = n

まず、 first_proofadd-clause コマンドを適用します。 add-clause コマンドによって、 first_proof の最初のパターンマッチが生成されます。

MyNat.idr
total first_proof : (n : MyNat) -> mynat_plus Zero n = n
first_proof n = ?first_proof_rhs

ここで、 ?first_proof_rhstype-of コマンドを適用します。 type-of コマンドによって ?first_proof_rhs の型が出力されます。

   n : MyNat
------------------------------
first_proof_rhs : n = n

あら、 n=n となっていますね。これはIdris側が mynat_plus Zero n を計算して n に置き換えてくれています。

mynat_plus の定義からも分かるように、第一引数が Zero の場合は第二引数が返り値となるので、 mynat_plus Zero n は自明に n と等しいということですね。したがって、 ?first_proof_rhs を型 n=n の値 Refl に置き換えて証明完了です。

MyNat.idr
total first_proof : (n : MyNat) -> mynat_plus Zero n = n
first_proof n = Refl

n+0=nを証明しよう

命題「すべての自然数 n に対して n+0=n である。」を型で表現すると次のようになります。

MyNat.idr
total second_proof : (n : MyNat) -> mynat_plus n Zero = n

まずは同様に、 second_proofadd-clause コマンドを適用します。

MyNat.idr
total second_proof : (n : MyNat) -> mynat_plus n Zero = n
second_proof n = ?second_proof_rhs

そして「n+0=nも自明でしょ!」と言いながら、 ?second_proof_rhs を型 n=n の値 Refl に置き換えちゃいましょう!

MyNat.idr
total second_proof : (n : MyNat) -> mynat_plus n Zero = n
second_proof = Refl

あ、、、怒られてしまいました。

nmynat_plus n Zero の間の制約が解けない(等価性の証明が与えられない)ようです。Idrisの気持ちになって、関数 mynat_plus の定義から mynat_plus n Zero を計算してみましょう。んー、できないですね。定数は計算してくれますが、変数が絡むと難しいみたいですね。では、どのように証明をすれば良いでしょうか?

数学的帰納法を用います。
n = \texttt{Zero}のとき

mynat_plus Zero Zero = Zero

ここで、n = \texttt{k}のとき成り立つと仮定する。

mynat_plus k Zero = k

n = \texttt{Succ k}のとき

mynat_plus (Succ k) Zero = Succ (mynat_plus k Zero)  (mynat_plusの定義より)
                         = Succ k  (仮定より)

①, ②からすべての自然数nについてn+0=nが成り立つ。

数学好きな皆さんにはお馴染みの証明ですね。以上の証明をプログラムに落とし込むと次のようになります。

MyNat.idr
total second_proof : (n : MyNat) -> mynat_plus n Zero = n
second_proof Zero = Refl
second_proof (Succ k) = 
    let hyp = second_proof k -- 帰納法の仮定
    in ?hole1

let で変数を定義することで in の中で使うことができます。ここで、 type-of コマンドを使用して ?hole1 の型をみてみましょう。

   k : MyNat
   hyp : mynat_plus k Zero = k
------------------------------
hole1 : Succ (mynat_plus k Zero) = Succ k

仮定 hyp から mynat_plus k Zerok が等しいことが使えるので、 hole1 : Succ (mynat_plus k Zero) = Succ kmynat_plus k Zerok に書き換えればいいですね!
どうやって? rewrite を使います。( let と同様に in の中で使われます。)

MyNat.idr
total second_proof : (n : MyNat) -> mynat_plus n Zero = n
second_proof Zero = Refl
second_proof (Succ k) = 
    let hyp = second_proof k -- 帰納法の仮定
    in rewrite hyp in ?hole2

最後に ?hole2 の型をみてみましょう。

   k : MyNat
   hyp : mynat_plus k Zero = k
------------------------------
hole2 : Succ k = Succ k

よし!きた!!型 Succ k = Succ k の値 Refl に書き換えれば証明完了です!

MyNat.idr
total second_proof : (n : MyNat) -> mynat_plus n Zero = n
second_proof Zero = Refl
second_proof (Succ k) = 
    let hyp = second_proof k -- 帰納法の仮定
    in rewrite hyp in Refl

おわりに

誤りや誤解を生みかねない表現などがありましたら、ご指摘よろしくお願いいたいします。

参考文献

  • Introduction to Dependent Types with Idris
  • Type-driven Development with Idris

Discussion