Lists_J: 直積、リスト、オプション

次の行を実行すると、前章の定義を一度にインポートすることができます。

Require Export Basics_J.

ただしこれを使うには、coqcを使ってBasics_J.vをコンパイルし、Basics_J.voを作成しておく必要があります。(これは、 .java ファイルから .class ファイルを作ったり、 .c ファイルから .o ファイルを作ったりするのと同じことです。)

コードをコンパイルする方法はふたつあります。

  • CoqIDE:Basics_J.v を開き、 “Compile” メニューの “Compile Buffer” をクリックする。
  • コマンドライン:coqc Basics_J.vを実行する。

このファイルでもModule機能を使って数のリストやペアの定義を囲んでおきます。こうしておくことで、同じ操作を改良した(一般化した)ものに同じ名前をつけることができます。

Module NatList.

数のペア

Inductiveによる型定義では、各構成子は任意の個数の引数を取ることができました。trueOのように引数のないもの、Sのようにひとつのもの、また、ふたつ以上の取るものも以下のように定義することができます。

Inductive natprod : Type :=
  pair : nat -> nat -> natprod.

この定義は以下のように読めます。すなわち、「数のペアを構成する方法がただひとつある。それは、構成子pairnat型のふたつの引数に適用することである」。

次に示すのは二引数の構成子に対してパターンマッチをする簡単な関数の定義です。

Definition fst (p : natprod) : nat :=
  match p with
  | pair x y => x
  end.
Definition snd (p : natprod) : nat :=
  match p with
  | pair x y => y
  end.

ペアはよく使うものなので、pair x yではなく、数学の標準的な記法で(x, y)と書けるとよいでしょう。このような記法を使うためにはNotation宣言を使います。

Notation "( x , y )" := (pair x y).

こうして定義した新しい記法(notation)は、式だけでなくパターンマッチに使うこともできます。(実際には、前章でも見たように、この記法は標準ライブラリの一部として提供されています。)

Eval simpl in (fst (3,4)).

Definition fst' (p : natprod) : nat :=
  match p with
  | (x,y) => x
  end.
Definition snd' (p : natprod) : nat :=
  match p with
  | (x,y) => y
  end.

Definition swap_pair (p : natprod) : natprod :=
  match p with
  | (x,y) => (y,x)
  end.

それでは、数のペアに関する簡単な事実をいくつか証明してみましょう。補題を一定の(一種独特な)形式で書いておけば、単に reflexivity(と組み込みの簡約)だけで証明することができます。

Theorem surjective_pairing' : forall (n m : nat),
  (n,m) = (fst (n,m), snd (n,m)).
Proof.
  reflexivity.  Qed.

しかし、補題を以下のようにより自然な書き方をした場合は、反射律では足りません。

Theorem surjective_pairing_stuck : forall (p : natprod),
  p = (fst p, snd p).
Proof.
  simpl.
Admitted.

simplfstsndの中のパターンマッチを実行できるよう、pの構造を明らかにする必要があります。これにはdestructを使います。

natの場合と異なり、destructでサブゴールが増えることはありません。これは、natprodの構成法がひとつしかないからです。

Theorem surjective_pairing : forall (p : natprod),
  p = (fst p, snd p).
Proof.
  intros p.  destruct p as (n,m).  simpl.  reflexivity.  Qed.

先ほど宣言した記法を “as...” パターンで束縛する変数を指定するために使っています。

練習問題: ★ (snd_fst_is_swap)

Theorem snd_fst_is_swap : forall (p : natprod),
  (snd p, fst p) = swap_pair p.
Proof.
  (* FILL IN HERE *) Admitted.

数のリスト

ペアの定義を少し一般化すると、数のリストは次のように表すことができます。すなわち、「リストは、空のリストであるか、または数と他のリストをペアにしたものである」。

Inductive natlist : Type :=
  | nil : natlist
  | cons : nat -> natlist -> natlist.

たとえば、次の定義は要素が三つのリストです

Definition l_123 := cons 1 (cons 2 (cons 3 nil)).

ペアの場合と同じく、リストをプログラミング言語で馴染んだ記法で書くことができると便利でしょう。次のふたつの宣言では::を中置のcons演算子として使えるようにし、角括弧をリストを構成するための外置(outfix)記法として使えるようにしています。

Notation "x :: l" := (cons x l) (at level 60, right associativity).
Notation "[ ]" := nil.
Notation "[ x , .. , y ]" := (cons x .. (cons y nil) ..).

この宣言を完全に理解する必要はありませんが、興味のある読者のために簡単に説明しておきます。

right associativityアノテーションは複数の::を使った式にどのように括弧を付けるか指示するものです。例えば、次のみっつの宣言はすべて同じ意味に解釈されます。

Definition l_123'   := 1 :: (2 :: (3 :: nil)).
Definition l_123''  := 1 :: 2 :: 3 :: nil.
Definition l_123''' := [1,2,3].

at level 60の部分は::を他の中置演算子といっしょに使っている式にどのように括弧を付けるかを指示するものです。例えば、+plusに対する level 50 の中置記法として定義したので、

Notation "x + y" := (plus x y)
                    (at level 50, left associativity).

+::よりも強く結合し、1 + 2 :: [3] は期待通り、1 + (2 :: [3)] ではなく(1 + 2) :: [3] と構文解析されます。

(ところで、 .v ファイルを読んでいるときには “1 + 2 :: [3]” のような書き方は少し読みにくいように感じるでしょう。内側の 3 の左右の角括弧はリストを表すものですが、外側の括弧は coqdoc 用の命令で、角括弧内の部分をそのままのテキストではなく Coq のコードとして表示するよう指示するものです。この角括弧は生成された HTML には現れません。)

上の二番目と三番目のNotation宣言は標準的なリストの記法を導入するためのものです。三番目のNotationの右辺は、 n 引数の記法を二項構成子の入れ子に変換する記法を定義するための Coq の構文の例です。

リストを操作するために便利な関数がいくつかあります。例えば、repeat関数は数ncountを取り、各要素がnで長さcountのリストを返します。

Fixpoint repeat (n count : nat) : natlist :=
  match count with
  | O => nil
  | S count' => n :: (repeat n count')
  end.

length関数はリストの長さを計算します。

Fixpoint length (l:natlist) : nat :=
  match l with
  | nil => O
  | h :: t => S (length t)
  end.

app(”append”)関数はふたつのリストを連結します。

Fixpoint app (l1 l2 : natlist) : natlist :=
  match l1 with
  | nil    => l2
  | h :: t => h :: (app t l2)
  end.

appはこの後でよく使うので、中置演算子を用意しておくと便利でしょう。

Notation "x ++ y" := (app x y)
                     (right associativity, at level 60).

Example test_app1:             [1,2,3] ++ [4,5] = [1,2,3,4,5].
Proof. reflexivity.  Qed.
Example test_app2:             nil ++ [4,5] = [4,5].
Proof. reflexivity.  Qed.
Example test_app3:             [1,2,3] ++ nil = [1,2,3].
Proof. reflexivity.  Qed.

もうふたつリストを使った例を見てみましょう。hd関数はリストの最初の要素(先頭—— head)を返し、tailは最初の要素を除いたものを返します。空のリストには最初の要素はありませんから、その場合に返す値を引数として渡しておかなければなりません。

Definition hd (default:nat) (l:natlist) : nat :=
  match l with
  | nil => default
  | h :: t => h
  end.

Definition tail (l:natlist) : natlist :=
  match l with
  | nil => nil
  | h :: t => t
  end.

Example test_hd1:             hd 0 [1,2,3] = 1.
Proof. reflexivity.  Qed.
Example test_hd2:             hd 0 [] = 0.
Proof. reflexivity.  Qed.
Example test_tail:            tail [1,2,3] = [2,3].
Proof. reflexivity.  Qed.

練習問題: ★★ (alternate)

alternateの定義を完成させなさい。この関数は、ふたつのリストから交互に要素を取り出しひとつに「綴じ合わせる」関数です。具体的な例は下のテストを見てください。

注意:alternateの自然な定義のひとつは、 「Fixpointによる定義は『明らかに停止する』ものでなければならない」という Coq の要求を満たすことができません。このパターンにはまってしまったようであれば、両方のリストの要素を同時に見ていくような少し冗長な方法を探してみてください。

Fixpoint alternate (l1 l2 : natlist) : natlist :=
  (* FILL IN HERE *) admit.

Example test_alternate1:        alternate [1,2,3] [4,5,6] = [1,4,2,5,3,6].
 (* FILL IN HERE *) Admitted.
Example test_alternate2:        alternate [1] [4,5,6] = [1,4,5,6].
 (* FILL IN HERE *) Admitted.
Example test_alternate3:        alternate [1,2,3] [4] = [1,4,2,3].
 (* FILL IN HERE *) Admitted.
Example test_alternate4:        alternate [] [20,30] = [20,30].
 (* FILL IN HERE *) Admitted.

リストを使ったバッグ

バッグ(bag。または多重集合——multiset)は集合のようなものですが、それぞれの要素が一度ではなく複数回現れることのできるようなものを言います。バッグの実装としてありうるのは数のバッグをリストで表現するというものでしょう。

Definition bag := natlist.

練習問題: ★★★ (bag_functions)

バッグに対するcountsumaddmember関数の定義を完成させなさい。

Fixpoint count (v:nat) (s:bag) : nat :=
  (* FILL IN HERE *) admit.

下の証明はすべてreflexivityだけでできます。

Example test_count1:              count 1 [1,2,3,1,4,1] = 3.
 (* FILL IN HERE *) Admitted.
Example test_count2:              count 6 [1,2,3,1,4,1] = 0.
 (* FILL IN HERE *) Admitted.

多重集合のsum(直和。または非交和)は集合のunion(和)と同じようなものです。sum a babの両方の要素を持つ多重集合です。(数学者は通常、多重集合のunionにもう少し異なる定義を与えます。それが、この関数の名前をunionにしなかった理由です。)sumのヘッダには引数の名前を与えませんでした。さらに、FixpointではなくDefinitionを使っています。ですから、引数に名前がついていたとしても再帰的な処理はできません。問題をこのように設定したのは、sumを(定義済みの関数を使うといった)別の方法で定義できないか考えさせるためです。

Definition sum : bag -> bag -> bag :=
  (* FILL IN HERE *) admit.

Example test_sum1:              count 1 (sum [1,2,3] [1,4,1]) = 3.
 (* FILL IN HERE *) Admitted.

Definition add (v:nat) (s:bag) : bag :=
  (* FILL IN HERE *) admit.

Example test_add1:                count 1 (add 1 [1,4,1]) = 3.
 (* FILL IN HERE *) Admitted.
Example test_add2:                count 5 (add 1 [1,4,1]) = 0.
 (* FILL IN HERE *) Admitted.

Definition member (v:nat) (s:bag) : bool :=
  (* FILL IN HERE *) admit.

Example test_member1:             member 1 [1,4,1] = true.
 (* FILL IN HERE *) Admitted.
Example test_member2:             member 2 [1,4,1] = false.
 (* FILL IN HERE *) Admitted.

練習問題: ★★★, optional (bag_more_functions)

練習として、さらにいくつかの関数を作成してください。

Fixpoint remove_one (v:nat) (s:bag) : bag :=


  (* FILL IN HERE *) admit.

Example test_remove_one1:         count 5 (remove_one 5 [2,1,5,4,1]) = 0.
 (* FILL IN HERE *) Admitted.
Example test_remove_one2:         count 5 (remove_one 5 [2,1,4,1]) = 0.
 (* FILL IN HERE *) Admitted.
Example test_remove_one3:         count 4 (remove_one 5 [2,1,4,5,1,4]) = 2.
 (* FILL IN HERE *) Admitted.
Example test_remove_one4:
  count 5 (remove_one 5 [2,1,5,4,5,1,4]) = 1.
 (* FILL IN HERE *) Admitted.

Fixpoint remove_all (v:nat) (s:bag) : bag :=
  (* FILL IN HERE *) admit.

Example test_remove_all1:          count 5 (remove_all 5 [2,1,5,4,1]) = 0.
 (* FILL IN HERE *) Admitted.
Example test_remove_all2:          count 5 (remove_all 5 [2,1,4,1]) = 0.
 (* FILL IN HERE *) Admitted.
Example test_remove_all3:          count 4 (remove_all 5 [2,1,4,5,1,4]) = 2.
 (* FILL IN HERE *) Admitted.
Example test_remove_all4:          count 5 (remove_all 5 [2,1,5,4,5,1,4,5,1,4]) = 0.
 (* FILL IN HERE *) Admitted.

Fixpoint subset (s1:bag) (s2:bag) : bool :=
  (* FILL IN HERE *) admit.

Example test_subset1:              subset [1,2] [2,1,4,1] = true.
 (* FILL IN HERE *) Admitted.
Example test_subset2:              subset [1,2,2] [2,1,4,1] = false.
 (* FILL IN HERE *) Admitted.

リストに関する推論

数の場合と同じく、リスト処理関数についての簡単な事実はもっぱら簡約のみで証明できることがあります。たとえば、次の定理はreflexivityで行われる簡約だけで証明できます。

Theorem nil_app : forall l:natlist,
  [] ++ l = l.
Proof.
   reflexivity.  Qed.

これは、[] がappの定義のパターンマッチ部分に代入され、パターンマッチ自体が簡約できるようになるからです。

またこれも数の場合と同じように、未知のリストの形(空であるかどうか)に応じた場合分けも有効です。

Theorem tl_length_pred : forall l:natlist,
  pred (length l) = length (tail l).
Proof.
  intros l. destruct l as [| n l'].
  Case "l = nil".
    reflexivity.
  Case "l = cons n l'".
    reflexivity.  Qed.

ここで、nilの場合がうまく行くのは、tl nil = nilと定義したからです。ここでは、destructタクティックのasnl'のふたつの名前を導入しました。これは、リストのcons構成子が引数をふたつ(構成するリストの頭部と尾部)取ることに対応しています。

ただし、リストに関する興味深い定理の証明には、帰納法が必要になるのが普通です。

単に例題の証明を読んでいるだけでは大きな進歩は望めません! 各証明を実際に Coq で動かし、各ステップがその証明にどのようにかかわっているか考え、道筋をていねいになぞっていくことがとても大切です。そうしなければ、演習には何の意味もありません。

natlistのようなデータ型に対して帰納法で証明をするのは、普通の自然数に対する帰納法よりも馴染みにくさを感じたことでしょう。しかし、基本的な考え方は同じくらい簡単です。Inductive宣言では、宣言した構成子から構築できるデータ方の集合を定義しています。例えば、ブール値ではtruefalseのいずれかであり、数ではOか数に対するSのいずれか、リストであればnilか数とリストに対するconsのいずれかです。

さらに言えば、帰納的に定義された集合の要素になるのは、宣言した構成子を互いに適用したものだけです。このことがそのまま帰納的に定義された集合に関する推論の方法になります。すなわち、数はOであるか、より小さい数にSを適用したものであるかのいずれかです。リストはnilであるか、何らかの数とより小さいリストにconsを適用したものです。他のものも同様です。ですから、あるリストlに関する命題Pがあり、Pがすべてのリストに対して成り立つことを示したい場合には、次のように推論します。

  • まず、lnilのときPlについて成り立つことを示す。
  • それから、lcons n l'であるとき、ある数nとより小さいリストl'に対して、Pl'について成り立つと仮定すればPlについても成り立つことを示す。

大きなリストはそれより小さなリストから作り出され、少しずつnilに近付いて行きます。よって、このふたつのことからすべてのリストlに関してPが真であることが言えます。具体的な例で説明しましょう。

Theorem app_ass : forall l1 l2 l3 : natlist,
  (l1 ++ l2) ++ l3 = l1 ++ (l2 ++ l3).
Proof.
  intros l1 l2 l3. induction l1 as [| n l1'].
  Case "l1 = nil".
    reflexivity.
  Case "l1 = cons n l1'".
    simpl. rewrite -> IHl1'. reflexivity.  Qed.

蒸し返すようですが、この Coq の証明はこうして単に静的なテキストとして読んでいる限り、さほど明白で分かりやすいものではありません。 Coq の証明は、 Coq を対話的に動かしながらポイントごとに「現在のゴールは何か」「コンテキストに何が出ているか」を見て、証明が今どうなっているかを読み下していくことで理解されるようになっています。しかし、このような証明の途中経過は、全てが証明結果として書き出されるわけではありません。だからこそ、人間向けの自然言語での証明には証明の筋道がわかるように証明の指針を書いておく必要があるのです。特に、読者が流れを見失わないよう、ふたつめの場合分けで使う帰納法の仮定が何だったのかわかるようにしておくのは有益なはずです。

定理: 任意のリストl1l2l3について、(l1 ++ l2) ++ l3 = l1 ++ (l2 ++ l3)が成り立つ。

証明:l1についての帰納法で証明する

  • まず、l1 = [] と仮定して

    ([] ++ l2) ++ l3 = [] ++ (l2 ++ l3)
    

を示す。これは++の定義から自明である。

  • 次にl1 = n::l1'かつ

    (l1' ++ l2) ++ l3 = l1' ++ (l2 ++ l3)
    

(帰納法の仮定)と仮定して

((n :: l1') ++ l2) ++ l3 = (n :: l1') ++ (l2 ++ l3)

を示す。++の定義から、この式は以下のように変形できる。

n :: ((l1' ++ l2) ++ l3) = n :: (l1' ++ (l2 ++ l3))

これは帰納法の仮定から直接導かれる。☐

下の練習問題は授業中に解きましょう。

Theorem app_length : forall l1 l2 : natlist,
  length (l1 ++ l2) = (length l1) + (length l2).
Proof.

  intros l1 l2. induction l1 as [| n l1'].
  Case "l1 = nil".
    reflexivity.
  Case "l1 = cons".
    simpl. rewrite -> IHl1'. reflexivity.  Qed.

リストに対する帰納的証明のもう少し入り組んだ例を見てみましょう。リストの右側にconsする関数snocを定義したとしましょう。

Fixpoint snoc (l:natlist) (v:nat) : natlist :=
  match l with
  | nil    => [v]
  | h :: t => h :: (snoc t v)
  end.

この関数を使ってリストの反転関数revを定義します。

Fixpoint rev (l:natlist) : natlist :=
  match l with
  | nil    => nil
  | h :: t => snoc (rev t) h
  end.

Example test_rev1:            rev [1,2,3] = [3,2,1].
Proof. reflexivity.  Qed.
Example test_rev2:            rev nil = nil.
Proof. reflexivity.  Qed.

新しく定義したsnocrevに関する定理を証明してみましょう。ここまでの帰納的証明よりも難易度の高いものですが、リストを反転しても長さの変わらないことを証明します。下の方法では、ふたつめの場合分けで行き詰まってしまいます。

Theorem rev_length_firsttry : forall l : natlist,
  length (rev l) = length l.
Proof.
  intros l. induction l as [| n l'].
  Case "l = []".
    reflexivity.
  Case "l = n :: l'".
    simpl.

Admitted.

このsnocに関する等式が成り立つことを示せれば証明が先に進むはずです。この式を取り出して別個の補題として証明してみましょう。

Theorem length_snoc : forall n : nat, forall l : natlist,
  length (snoc l n) = S (length l).
Proof.
  intros n l. induction l as [| n' l'].
  Case "l = nil".
    reflexivity.
  Case "l = cons n' l'".
    simpl. rewrite -> IHl'. reflexivity.  Qed.

これで、元々の証明ができるようになりました。

Theorem rev_length : forall l : natlist,
  length (rev l) = length l.
Proof.
  intros l. induction l as [| n l'].
  Case "l = nil".
    reflexivity.
  Case "l = cons".
    simpl. rewrite -> length_snoc.
    rewrite -> IHl'. reflexivity.  Qed.

対比として、この二つの定理の非形式的な証明を見てみましょう

定理: 任意の数nとリストlについてlength (snoc l n) = S (length l)が成り立つ。

証明:lについての帰納法で証明する。

  • まず、l = [] と仮定して

    length (snoc [] n) = S (length [])
    

を示す。これはlengthsnocの定義から直接導かれる。

  • 次に、l = n'::l'かつ

    length (snoc l' n) = S (length l')
    

と仮定して、

length (snoc (n' :: l') n) = S (length (n' :: l'))

を示す。lengthsnocの定義から次のように変形できる。

S (length (snoc l' n)) = S (S (length l'))

これは帰納法の仮定から明らかである。☐

定理: 任意のリストlについてlength (rev l) = length lが成り立つ。

証明:lについての帰納法で証明する。

  • まず、l = [] と仮定して

    length (rev []) = length []
    

を示す。これはlengthrevの定義から直接導かれる

  • 次に、l = n::l'かつ

    length (rev l') = length l'
    

と仮定して、

length (rev (n :: l')) = length (n :: l')

を示す。revの定義から次のように変形できる。

length (snoc (rev l') n) = S (length l')

これは、先程の補題から、次のものと同じである。

S (length (rev l')) = S (length l')

これは、帰納法の仮定から明らかである。☐

こういった証明のスタイルは、どう見ても長ったらしく杓子定規な感じがします。最初の何回かは別にして、それ以後は、細かいところは省略してしまって(必要であれば、頭の中や紙の上で追うのは簡単です)、自明でないところにだけ注目した方がわかりやすいでしょう。そのように省略がちに書けば、上の証明は次のようになります。

定理:任意のリストlについてlength (rev l) = length lが成り立つ。

証明: まず、任意のlについて

length (snoc l n) = S (length l)

であることに注目する。これはlについての帰納法から自明である。このとき、もとの性質についてもlについての帰納法から自明である。l = n'::l'の場合については、上の性質と帰納法の仮定から導かれる。☐

どちらのスタイルの方が好ましいかは、読み手の証明への馴れや、彼らが今まで触れてきた証明がどちらに近いかに依ります。本書の目的としては冗長なスタイルの方が無難でしょう。

これまで見てきたように、定理を証明するには既に証明した定理を使うことができます。以降ではrewrite以外にも、証明済みの定理を使う方法があることを紹介します。ところで、定理を使うためにはその名前を知らなければなりませんが、使えそうな定理の名前をすべて覚えておくのはとても大変です。今まで証明した定理を覚えておくだけでも大変なのに、その名前となったら尚更です。

Coq のSearchAboutコマンドはこのような場合にとても便利です。SearchAbout fooとすると、fooに関する証明がすべて表示されます。例えば、次の部分のコメントを外せば、これまでrevに関して証明した定理が表示されます。

続く練習問題やコースに取り組む際には、常にSearchAboutコマンドのことを頭の隅に置いておくといいでしょう。そうすることでずいぶん時間の節約ができるはずです。

もし ProofGeneral を使っているのなら、C-c C-fとキー入力をすることでSearchAboutコマンドを使うことができます。その結果をエディタに貼り付けるにはC-c C-;を使うことができます。

練習問題: ★★, optional (bag_proofs)

前のバッグについての optional な練習問題に挑戦したのであれば、その定義について、以下の定理を証明しなさい。

Theorem count_member_nonzero : forall (s : bag),
  ble_nat 1 (count 1 (1 :: s)) = true.
Proof.
  (* FILL IN HERE *) Admitted.

以下のble_natに関する補題は、この次の証明に使えるかもしれません。

Theorem ble_n_Sn : forall n,
  ble_nat n (S n) = true.
Proof.
  intros n. induction n as [| n'].
  Case "0".
    simpl.  reflexivity.
  Case "S n'".
    simpl.  rewrite IHn'.  reflexivity.  Qed.

Theorem remove_decreases_count: forall (s : bag),
  ble_nat (count 0 (remove_one 0 s)) (count 0 s) = true.
Proof.
  (* FILL IN HERE *) Admitted.

(* Write down an interesting theorem about bags involving the
    functions [count] and [sum], and prove it.

練習問題: ★★★, optional (bag_count_sum)

バッグについてcountsumを使った定理を考え、それを証明しなさい。

(* FILL IN HERE *)
[]
 *)

練習問題: ★★★★, optional (rev_injective)

rev関数が単射である、すなわち

forall X (l1 l2 : list X), rev l1 = rev l2 -> l1 = l2

であることを証明しなさい。

この練習問題には簡単な解法と難しい解法があります。

(* FILL IN HERE *)

オプション

次に、日々のプログラミングでも役に立つような型の定義を見てみましょう。

Inductive natoption : Type :=
  | Some : nat -> natoption
  | None : natoption.

natoption型の使い途のひとつは、関数からエラーコードを返すことです。例えば、リストのn番目の要素を返す関数を書きたいとしましょう。型をnat -> natlist -> natとしてしまったら、リストが短かすぎた場合でも何か適当な数を返さなければなりません!

Fixpoint index_bad (n:nat) (l:natlist) : nat :=
  match l with
  | nil => 42
  | a :: l' => match beq_nat n O with
               | true => a
               | false => index_bad (pred n) l'
               end
  end.

これに対して、型をnat -> natlist -> natoptionとすれば、リストが短かすぎた場合にはNoneを返し、リストが十分に長く、n番目の要素がaであった場合にはSome aを返すことができます。

Fixpoint index (n:nat) (l:natlist) : natoption :=
  match l with
  | nil => None
  | a :: l' => match beq_nat n O with
               | true => Some a
               | false => index (pred n) l'
               end
  end.

Example test_index1 :    index 0 [4,5,6,7]  = Some 4.
Proof. reflexivity.  Qed.
Example test_index2 :    index 3 [4,5,6,7]  = Some 7.
Proof. reflexivity.  Qed.
Example test_index3 :    index 10 [4,5,6,7] = None.
Proof. reflexivity.  Qed.

この機会に、 Coq のプログラミング言語としての機能として、条件式を紹介しておきましょう。

Fixpoint index' (n:nat) (l:natlist) : natoption :=
  match l with
  | nil => None
  | a :: l' => if beq_nat n O then Some a else index (pred n) l'
  end.

Coq の条件式(if式)は他の言語に見られるものとほとんど同じですが、少しだけ一般化されています。 Coq には 組み込みのブーリアン型がないため、 Coq の条件式では、実際には、構成子のふたつある任意の帰納型に対して分岐をすることができます。条件部の式がInductiveの定義の最初の構成子に評価された場合には真、ふたつめの構成子に評価された場合には偽と見做されます。

次の関数は、natoption型からnatの値を取り出し、Noneの場合には与えられたデフォルト値を返します。

Definition option_elim (o : natoption) (d : nat) : nat :=
  match o with
  | Some n' => n'
  | None => d
  end.

練習問題: ★★ (hd_opt)

同じ考え方を使って、以前定義したhd関数を修正し、nilの場合に返す値を渡さなくて済むようにしなさい。

Definition hd_opt (l : natlist) : natoption :=
  (* FILL IN HERE *) admit.

Example test_hd_opt1 : hd_opt [] = None.
 (* FILL IN HERE *) Admitted.

Example test_hd_opt2 : hd_opt [1] = Some 1.
 (* FILL IN HERE *) Admitted.

Example test_hd_opt3 : hd_opt [5,6] = Some 5.
 (* FILL IN HERE *) Admitted.

練習問題: ★★, optional (option_elim_hd)

新しいhd_optと古いhdの関係についての練習問題です。

Theorem option_elim_hd : forall (l:natlist) (default:nat),
  hd default l = option_elim (hd_opt l) default.
Proof.
  (* FILL IN HERE *) Admitted.

applyタクティック

証明をしていると、証明すべきゴールがコンテキスト中の仮定と同じであったり以前証明した補題と同じであることがしばしばあります。

Theorem silly1 : forall (n m o p : nat),
     n = m  ->
     [n,o] = [n,p] ->
     [n,o] = [m,p].
Proof.
  intros n m o p eq1 eq2.
  rewrite <- eq1.


  apply eq2.  Qed.

また、applyタクティックは、条件付きの仮定や補題にも使うことができます。適用するものに含意が含まれていれば、含意の前提部分が証明すべきサブゴールに加えられます。

Theorem silly2 : forall (n m o p : nat),
     n = m  ->
     (forall (q r : nat), q = r -> [q,o] = [r,p]) ->
     [n,o] = [m,p].
Proof.
  intros n m o p eq1 eq2.
  apply eq2. apply eq1.  Qed.

この証明で、applyの代わりにrewriteを使って証明を終えられるか試してみると有益でしょう。

apply Hを使う典型的な例は、Hforallで始まり、何らかの全称限量された変数を束縛している場合です。現在のゴールがHの帰結部と一致した場合には、変数に対応する適当な値を見つけてくれます。例えば、次の証明でapply eq2すると、eq2内の変数qnに、rmに具体化されます。

Theorem silly2a : forall (n m : nat),
     (n,n) = (m,m)  ->
     (forall (q r : nat), (q,q) = (r,r) -> [q] = [r]) ->
     [n] = [m].
Proof.
  intros n m eq1 eq2.
  apply eq2. apply eq1.  Qed.

練習問題: ★★, optional (silly_ex)

次の証明をsimplを使わずに完成させなさい。

Theorem silly_ex :
     (forall n, evenb n = true -> oddb (S n) = true) ->
     evenb 3 = true ->
     oddb 4 = true.
Proof.
  (* FILL IN HERE *) Admitted.

applyタクティックを使う場合には、適用する事実(の帰結部)が、ゴールと完全に一致していなければなりません。例えは、等式の左辺と右辺が入れ替わっているだけでもapplyタクティックは使えません。

Theorem silly3_firsttry : forall (n : nat),
     true = beq_nat n 5  ->
     beq_nat (S (S n)) 7 = true.
Proof.
  intros n H.
  simpl.


Admitted.

そのような場合にはsymmetryタクティックを使って、ゴールの等式の左辺と右辺を入れ替えることができます。

Theorem silly3 : forall (n : nat),
     true = beq_nat n 5  ->
     beq_nat (S (S n)) 7 = true.
Proof.
  intros n H.
  symmetry.
  simpl.

  apply H.  Qed.

練習問題: ★ (apply_rewrite)

applyrewriteの違いを簡単に説明しなさい。どちらもうまく使えるような場面はありますか?

(* FILL IN HERE *)

帰納法の仮定を変更する

帰納法による証明の微妙さについては説明しておく価値があるでしょう。例えば、以前証明したapp_assを見てみましょう。帰納法の仮定(inductionタクティックで生成されたふたつめのサブゴール)は以下のようなものでした。

(l1' ++ l2) ++ l3 = l1' ++ l2 ++ l3

++を右結合と定義したので、=の右辺はl1' ++ (l2 ++ l3)と同じです。)

この仮定は、l1'と、特定のリストl2l3に関するものです。l2l3はこの証明の初めにintrosタクティックで導入したもので、この仮定中で「一定」です。証明の方法を少し変えて、最初にnだけをコンテキストにintrosするようにしたら、帰納法の仮定は次のようにもっと強いものになります。

forall l2 l3,  (l1' ++ l2) ++ l3 = l1' ++ l2 ++ l3

Coq を使って実際に違いを確認してください。

今回の場合では、ふたつの証明の違いはささいものです。これは、++関数の定義が最初の引数だけを見て、ふたつめの引数には特に何もしないからです。しかし、遠からずわかることですが、帰納法の仮定をどちらにするかで証明の成否が分かれることもあるのです。

練習問題: ★★, optional (app_ass’)

++の結合則をより一般的な仮定のもとで証明しなさい。(最初の行を変更せずに)次の証明を完成させること。

Theorem app_ass' : forall l1 l2 l3 : natlist,
  (l1 ++ l2) ++ l3 = l1 ++ (l2 ++ l3).
Proof.
  intros l1. induction l1 as [ | n l1'].
  (* FILL IN HERE *) Admitted.

練習問題: ★★★ (apply_exercise2)

inductionの前にmintrosしていないことに注意してください。これによって仮定が一般化され、帰納法の仮定が特定のmに縛られることがなくなり、より使いやすくなりました。

Theorem beq_nat_sym : forall (n m : nat),
  beq_nat n m = beq_nat m n.
Proof.
  intros n. induction n as [| n'].
  (* FILL IN HERE *) Admitted.

[]
 *)

FILL IN HERE ##### 練習問題: ★★★, recommended (beq_nat_sym_informal)

以下の補題について上の証明と対応する非形式的な証明を書きなさい。

定理: 任意のnat``n``mについて、beq_nat n m = beq_nat m n

証明:(* FILL IN HERE *)☐

End NatList.

練習問題: 辞書

Module Dictionary.

Inductive dictionary : Type :=
  | empty  : dictionary
  | record : nat -> nat -> dictionary -> dictionary.

この宣言は次のように読めます。「dictionaryを構成する方法はふたつある。構成子emptyで空の辞書を表現するか、構成子recordをキーと値と既存のdictionaryに適用してキーと値の対応を追加したdictionaryを構成するかのいずれかである」。

Definition insert (key value : nat) (d : dictionary) : dictionary :=
  (record key value d).

下のfind関数は、dictionaryから与えられたキーに対応する値を探し出すものです。 キーが見つからなかった場合にはNoneに評価され、キーがvalに結び付けられていた場合にはSome valに評価されます。同じキーが複数の値に結び付けられている場合には、最初に見つかったほうの値を返します。

Fixpoint find (key : nat) (d : dictionary) : option nat :=
  match d with
  | empty         => None
  | record k v d' => if (beq_nat key k) then (Some v) else (find key d')
  end.

練習問題: ★ (dictionary_invariant1)

Theorem dictionary_invariant1 : forall (d : dictionary) (k v: nat),
  (find k (insert k v d)) = Some v.
Proof.
 (* FILL IN HERE *) Admitted.

練習問題: ★ (dictionary_invariant2)

Theorem dictionary_invariant2 : forall (d : dictionary) (m n o: nat),
  (beq_nat m n) = false -> (find m d) = (find m (insert n o d)).
Proof.
 (* FILL IN HERE *) Admitted.

End Dictionary.

次の宣言で、beq_nat_symの定義をトップレベルの名前空間に置いておきます。こうすることで、後でbeq_nat_symを使うのにNatList.beq_nat_symと書かずに済みます。

Definition beq_nat_sym := NatList.beq_nat_sym.