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


From LF Require Export Induction.
Module NatList.

数のペア


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

Inductive natprod : Type :=
| pair (n1 n2 : nat).

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

Check (pair 3 5).

ペアの第一要素と第二要素を取り出す関数を定義します。

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.

Compute (fst (pair 3 5)).

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

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

こうして定義したペアの新しい記法(notation)は、式だけでなくパターンマッチに使うこともできます。

Compute (fst (3,5)).

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.

Note that pattern-matching on a pair (with parentheses: (x, y)) is not to be confused with the "multiple pattern" syntax (with no parentheses: x, y) that we have seen previously.
The above examples illustrate pattern matching on a pair with elements x and y, whereas minus below (taken from Basics) performs pattern matching on the values n and m.
Fixpoint minus (n m : nat) : nat := match n, m with | O , _ => O | S _ , O => n | S n', S m' => minus n' m' end.
The distinction is minor, but it is worth knowing that they are not the same. For instance, the following definitions are ill-formed:
Definition bad_fst (p : natprod) : nat := match p with | x, y => x end.
Definition bad_minus (n m : nat) : nat := match n, m with | (O , _ ) => O | (S _ , O ) => n | (S n', S m') => bad_minus n' m' end.

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

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

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

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

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

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

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

練習問題: ★, standard (snd_fst_is_swap)

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

練習問題: ★, standard, optional (fst_swap_is_snd)

Theorem fst_swap_is_snd : forall (p : natprod),
  fst (swap_pair p) = snd p.
Proof.
Admitted.

数のリスト


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

Inductive natlist : Type :=
  | nil
  | cons (n : nat) (l : natlist).

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

Definition mylist := 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 mylist1 := 1 :: (2 :: (3 :: nil)).
Definition mylist2 := 1 :: 2 :: 3 :: nil.
Definition mylist3 := [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


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

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

Length


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

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

Append


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)を返し、 tl は最初の要素を除いたもの(後ろ―― tail)を返します。 空のリストには最初の要素はありませんから、その場合に返す値を引数として渡しておかなければなりません。

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

Definition tl (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_tl: tl [1;2;3] = [2;3].
Proof. reflexivity. Qed.

練習問題


練習問題: ★★, standard, recommended (list_funs)

以下の nonzerosoddmemberscountoddmembers の定義を完成させなさい。 どのように動くかは続くテストから考えてください。

Fixpoint nonzeros (l:natlist) : natlist
  . Admitted.

Example test_nonzeros:
  nonzeros [0;1;0;2;3;0;0] = [1;2;3].
Admitted.

Fixpoint oddmembers (l:natlist) : natlist
  . Admitted.

Example test_oddmembers:
  oddmembers [0;1;0;2;3;0;0] = [1;3].
Admitted.

Definition countoddmembers (l:natlist) : nat
  . Admitted.

Example test_countoddmembers1:
  countoddmembers [1;0;3;1;4;5] = 4.
Admitted.

Example test_countoddmembers2:
  countoddmembers [0;2;4] = 0.
Admitted.

Example test_countoddmembers3:
  countoddmembers nil = 0.
Admitted.

練習問題: ★★★, advanced (alternate)

alternate の定義を完成させなさい。 この関数は、ふたつのリストから交互に要素を取り出しひとつにまとめる関数です。 具体的な例は下のテストを見てください。
(注意: alternate の自然な定義のひとつは、 Coq の要求する「Fixpoint による定義は『明らかに停止する』ものでなければならない」という性質を満たすことができません。 このパターンにはまってしまったようであれば、両方のリストの要素を同時に見ていくような少し冗長な方法を探してみてください。 新しい対を定義する必要のある解法もありますが、それ以外にもあります。)

Fixpoint alternate (l1 l2 : natlist) : natlist
  . Admitted.

Example test_alternate1:
  alternate [1;2;3] [4;5;6] = [1;4;2;5;3;6].
Admitted.

Example test_alternate2:
  alternate [1] [4;5;6] = [1;4;5;6].
Admitted.

Example test_alternate3:
  alternate [1;2;3] [4] = [1;4;2;3].
Admitted.

Example test_alternate4:
  alternate [] [20;30] = [20;30].
Admitted.

リストを使ったバッグ


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

Definition bag := natlist.

練習問題: ★★★, standard, recommended (bag_functions)

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

Fixpoint count (v:nat) (s:bag) : nat
  . Admitted.

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

Example test_count1: count 1 [1;2;3;1;4;1] = 3.
Admitted.
Example test_count2: count 6 [1;2;3;1;4;1] = 0.
Admitted.

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

Definition sum : bag -> bag -> bag
  . Admitted.

Example test_sum1: count 1 (sum [1;2;3] [1;4;1]) = 3.
Admitted.

Definition add (v:nat) (s:bag) : bag
  . Admitted.

Example test_add1: count 1 (add 1 [1;4;1]) = 3.
Admitted.
Example test_add2: count 5 (add 1 [1;4;1]) = 0.
Admitted.

Definition member (v:nat) (s:bag) : bool
  . Admitted.

Example test_member1: member 1 [1;4;1] = true.
Admitted.

Example test_member2: member 2 [1;4;1] = false.
Admitted.

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

練習として、さらに以下のbag用の関数を作成しなさい。

remove_one を削除しようとしている数の入っていないバッグに適用した場合は、同じバッグを変更せずに返す。 (この練習問題はオプション問題ですが、後ろの発展問題で remove_one の定義を必要とするものがあります。)

Fixpoint remove_one (v:nat) (s:bag) : bag
  . Admitted.

Example test_remove_one1:
  count 5 (remove_one 5 [2;1;5;4;1]) = 0.
Admitted.

Example test_remove_one2:
  count 5 (remove_one 5 [2;1;4;1]) = 0.
Admitted.

Example test_remove_one3:
  count 4 (remove_one 5 [2;1;4;5;1;4]) = 2.
Admitted.

Example test_remove_one4:
  count 5 (remove_one 5 [2;1;5;4;5;1;4]) = 1.
Admitted.

Fixpoint remove_all (v:nat) (s:bag) : bag
  . Admitted.

Example test_remove_all1: count 5 (remove_all 5 [2;1;5;4;1]) = 0.
Admitted.
Example test_remove_all2: count 5 (remove_all 5 [2;1;4;1]) = 0.
Admitted.
Example test_remove_all3: count 4 (remove_all 5 [2;1;4;5;1;4]) = 2.
Admitted.
Example test_remove_all4: count 5 (remove_all 5 [2;1;5;4;5;1;4;5;1;4]) = 0.
Admitted.

Fixpoint subset (s1:bag) (s2:bag) : bool
  . Admitted.

Example test_subset1: subset [1;2] [2;1;4;1] = true.
Admitted.
Example test_subset2: subset [1;2;2] [2;1;4;1] = false.
Admitted.

練習問題: ★★★, standard, recommended (bag_theorem)

countadd を使ったバッグに関する面白い定理を書き、それを証明しなさい。 この問題はいわゆる自由課題で、真であっても、証明にまだ習っていない技法を使わなければならない定理を思いついてしまうかもしれません。 証明に行き詰まってしまったら気軽に質問してください。

リストに関する推論


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

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

これは、 []app の定義における「被検査体(scrutinee)」(パターンマッチでその値が「検査(scrutinize)」される式 / 訳注:matchwithの間に書いた式)に代入され、パターンマッチが簡約できるようになるからです。

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

Theorem tl_length_pred : forall l:natlist,
  pred (length l) = length (tl l).
Proof.
  intros l. destruct l as [| n l'].
  -
    reflexivity.
  -
    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_assoc : forall l1 l2 l3 : natlist,
  (l1 ++ l2) ++ l3 = l1 ++ (l2 ++ l3).
Proof.
  intros l1 l2 l3. induction l1 as [| n l1' IHl1'].
  -
    reflexivity.
  -
    simpl. rewrite -> IHl1'. reflexivity. Qed.

自然数上の帰納法と同様に、inductionタクティックでのas ...節はconsの場合の残りのリストにl1'と名前を付けるために使われています。 蒸し返すようですが、この 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))
    これは帰納法の仮定から直接導かれる。

リストの反転


より入り組んだリストに関する帰納法による証明の例として、リストを反転させる関数revを使います。 revの定義にはappを使うことにします。

Fixpoint rev (l:natlist) : natlist :=
  match l with
  | nil => nil
  | h :: t => 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.

rev の性質


ここまで見てきたものよりも難易度の高いものですが、リストを反転しても長さの変わらないことを証明します。 下の方法では、ふたつめの場合分けで行き詰まってしまいます。

Theorem rev_length_firsttry : forall l : natlist,
  length (rev l) = length l.
Proof.
  intros l. induction l as [| n l' IHl'].
  -
    reflexivity.
  -
    
    simpl.
    rewrite <- IHl'.
Abort.

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

Theorem app_length : forall l1 l2 : natlist,
  length (l1 ++ l2) = (length l1) + (length l2).
Proof.
  intros l1 l2. induction l1 as [| n l1' IHl1'].
  -
    reflexivity.
  -
    simpl. rewrite -> IHl1'. reflexivity. Qed.

補題をできる限り一般的なものにするため、revの定義で出てくるような結果に対してではなく、「全ての」natlistに対するものとして形式化しました。 目的としたゴールは明らかにリストの反転という状態に全く依存していないという点で、この一般化は自然なものでしょう。 さらに、この一般的な性質の方がより簡単に証明できます。

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

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

対比として、この二つの定理の非形式的な証明を見てみましょう
定理: 二つのリスト l1l2 について、 length (l1 ++ l2) = length l1 + length l2 が成り立つ。
証明: l1 についての帰納法で証明する。
  • まず、 l1 = [] と仮定して
            length ([] ++ l2) = length [] + length l2
    を示す。これは length++ の定義から直接導かれる。
  • 次に、 l1 = n::l1' かつ
            length (l1' ++ l2) = length l1' + length l2
    と仮定して、
            length ((n::l1') ++ l2) = length (n::l1') + length l2)
    を示す。これは length++ の定義および帰納法の仮定から明らかである。

定理: 任意のリスト 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 ((rev l') ++ [n]) = S (length l')
    これは、先程の補題から、次のものと同じである。
              length (rev l') + length [n] = S (length l')
    これは、length の定義および帰納法の仮定から明らかである。

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

定理: 任意のリスト l について length (rev l) = length l が成り立つ。
証明: まず、任意の l について length (l ++ [n]) = S (length l) であることに注目する(これは l についての帰納法から自明である)。 もとの性質については、 l についての帰納法から示し、 l = n'::l' の場合については、上の性質と帰納法の仮定から導かれる。

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

Search


これまで見てきたように、定理を証明するには既に証明した定理を、例えばrewriteを通じて、使うことができます。 しかし、定理を使うためにはその名前を知らなければなりません! 使えそうな定理の名前をすべて覚えておくのはとても大変です。 今まで証明した定理を覚えておくだけでも大変なのに、その名前となったら尚更です。
Coq の Search コマンドはこのような場合にとても便利です。 スクリプトファイルに Search foo と書いて評価させると、 foo に関する証明がすべて表示されます。 例えば、次の部分のコメントを外せば、これまで rev に関して証明した定理が表示されます。


続く練習問題に取り組む際や読み進める際には、常に Search コマンドのことを頭の隅に置いておくといいでしょう。 そうすることでずいぶん時間の節約ができるはずです。
もし ProofGeneral を使っているのなら、 C-c C-a C-a とキー入力をすることで Search コマンドを使うことができます。 その結果をエディタに貼り付けるには C-c C-; を使うことができます。

リストについての練習問題 (1)


練習問題: ★★★, standard (list_exercises)

リストについてさらに練習しましょう。

Theorem app_nil_r : forall l : natlist,
  l ++ [] = l.
Proof.
Admitted.

Theorem rev_app_distr: forall l1 l2 : natlist,
  rev (l1 ++ l2) = rev l2 ++ rev l1.
Proof.
Admitted.

Theorem rev_involutive : forall l : natlist,
  rev (rev l) = l.
Proof.
Admitted.

次の問題には簡単な解法があります。 こんがらがってしまったようであれば、少し戻って単純な方法を探してみましょう。

Theorem app_assoc4 : forall l1 l2 l3 l4 : natlist,
  l1 ++ (l2 ++ (l3 ++ l4)) = ((l1 ++ l2) ++ l3) ++ l4.
Proof.
Admitted.

前に書いた nonzeros 関数に関する練習問題です。

Lemma nonzeros_app : forall l1 l2 : natlist,
  nonzeros (l1 ++ l2) = (nonzeros l1) ++ (nonzeros l2).
Proof.
Admitted.

練習問題: ★★, standard (eqblist)

次のeqblistは数のリストの等しさを計算するものです。 定義を埋めなさい。 また、どんなリストlに対してもeqblist l ltrueを返すことを示しなさい。

Fixpoint eqblist (l1 l2 : natlist) : bool
  . Admitted.

Example test_eqblist1 :
  (eqblist nil nil = true).
Admitted.

Example test_eqblist2 :
  eqblist [1;2;3] [1;2;3] = true.
Admitted.

Example test_eqblist3 :
  eqblist [1;2;3] [1;2;4] = false.
Admitted.

Theorem eqblist_refl : forall l:natlist,
  true = eqblist l l.
Proof.
Admitted.

リストについての練習問題 (2)


上で見たバッグについて、いくつかの定理を証明します。

練習問題: ★, standard (count_member_nonzero)

Theorem count_member_nonzero : forall (s : bag),
  1 <=? (count 1 (1 :: s)) = true.
Proof.
Admitted.

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

Theorem leb_n_Sn : forall n,
  n <=? (S n) = true.
Proof.
  intros n. induction n as [| n' IHn'].
  -
    simpl. reflexivity.
  -
    simpl. rewrite IHn'. reflexivity. Qed.

次の練習問題を解く前に、remove_one の定義ができあがっていることを確認してください。

練習問題: ★★★, advanced (remove_does_not_increase_count)

Theorem remove_does_not_increase_count: forall (s : bag),
  (count 0 (remove_one 0 s)) <=? (count 0 s) = true.
Proof.
Admitted.

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

バッグについて countsum を使った定理 bag_count_sum を考え、それをCoqを使って証明しなさい。 (countの定義によって証明の難易度が変わるかもしれません。)

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

rev 関数が単射である、すなわち
    forall (l1 l2 : natlist), rev l1 = rev l2 -> l1 = l2
であることを証明しなさい。 (この練習問題には簡単な解法と難しい解法があります。)

オプション


リストからn番目を取り出す関数を定義するとします。 もし関数の型をnat -> natlist -> natにすると、リストが短すぎる(つまりnilだけの場合)何かの数値を選ばなければなりません。

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

この方法はあまり良くありません。 もしnth_bad42を返したとしても、別の処理なしにはそれが実際にリストにあった値なのかを判断できません。 より良い方法としては、nth_badの返し値の型を変更して、エラー値を扱えるようにするというものです。 この型をnatoptionと呼ぶことにします。

Inductive natoption : Type :=
  | Some (n : nat)
  | None.

We can then change the above definition of nth_bad to return None when the list is too short and Some a when the list has enough members and a appears at position n. We call this new function nth_error to indicate that it may result in an error.

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

Example test_nth_error1 : nth_error [4;5;6;7] 0 = Some 4.
Proof. reflexivity. Qed.
Example test_nth_error2 : nth_error [4;5;6;7] 3 = Some 7.
Proof. reflexivity. Qed.
Example test_nth_error3 : nth_error [4;5;6;7] 9 = None.
Proof. reflexivity. Qed.

(HTML版では上の例での証明部分の常套句が消えています。 もし見たければ直後の四角をクリックしてください。)
この機会に、 Coq のプログラミング言語としての機能として、条件式を紹介しておきましょう。

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

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

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

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

練習問題: ★★, standard (hd_error)

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

Definition hd_error (l : natlist) : natoption
  . Admitted.

Example test_hd_error1 : hd_error [] = None.
Admitted.

Example test_hd_error2 : hd_error [1] = Some 1.
Admitted.

Example test_hd_error3 : hd_error [5;6] = Some 5.
Admitted.

Exercise: 1 star, standard, optional (option_elim_hd)

This exercise relates your new hd_error to the old hd.

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

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

Theorem option_elim_hd : forall (l:natlist) (default:nat),
  hd default l = option_elim default (hd_error l).
Proof.
Admitted.

End NatList.

Partial Maps

As a final illustration of how data structures can be defined in Coq, here is a simple partial map data type, analogous to the map or dictionary data structures found in most programming languages.
First, we define a new inductive datatype id to serve as the "keys" of our partial maps.

Inductive id : Type :=
  | Id (n : nat).

Internally, an id is just a number. Introducing a separate type by wrapping each nat with the tag Id makes definitions more readable and gives us the flexibility to change representations later if we wish.
We'll also need an equality test for ids:

Definition eqb_id (x1 x2 : id) :=
  match x1, x2 with
  | Id n1, Id n2 => n1 =? n2
  end.

Exercise: 1 star, standard (eqb_id_refl)

Theorem eqb_id_refl : forall x, true = eqb_id x x.
Proof.
Admitted.
Now we define the type of partial maps:

Module PartialMap.
Export NatList.

Inductive partial_map : Type :=
  | empty
  | record (i : id) (v : nat) (m : partial_map).

この宣言は次のように読めます。 「partial_map を構成する方法はふたつある。 構成子 empty で空の部分写像を表現するか、構成子 record をキーと値と既存の partial_map に適用してキーと値の対応を追加した partial_map を構成するかのいずれかである。」
The update function overrides the entry for a given key in a partial map by shadowing it with a new one (or simply adds a new entry if the given key is not already present).

Definition update (d : partial_map)
                  (x : id) (value : nat)
                  : partial_map :=
  record x value d.

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

Fixpoint find (x : id) (d : partial_map) : natoption :=
  match d with
  | empty => None
  | record y v d' => if eqb_id x y
                     then Some v
                     else find x d'
  end.

Exercise: 1 star, standard (update_eq)

Theorem update_eq :
  forall (d : partial_map) (x : id) (v: nat),
    find x (update d x v) = Some v.
Proof.
Admitted.

Exercise: 1 star, standard (update_neq)

Theorem update_neq :
  forall (d : partial_map) (x y : id) (o: nat),
    eqb_id x y = false -> find x (update d y o) = find x d.
Proof.
Admitted.
End PartialMap.

Exercise: 2 stars, standard (baz_num_elts)

Consider the following inductive definition:

Inductive baz : Type :=
  | Baz1 (x : baz)
  | Baz2 (y : baz) (b : bool).

How many elements does the type baz have? (Explain in words, in a comment.)