Isabelle Tutorial その4

やっと証明に入りました。

2.3 An Introductory Proof

証明の大まかな流れを追ってみます。

Main Goal.

リストにrevを二回適用すると元に戻ることを証明します。

theorem rev_rev [simp]: "rev (rev xs) = xs"

theoremは新しい定理"rev (rev xs) = xs"を証明します。定理にはrev_revという名前を付けています。[simp]はこの定理を後から単純化ルールとして使うことを示しています。これは、単純化の際にrev (rev xs)xsに置き換えることを意味します。定理名と単純化の属性はオプションです。

これを入力すると、Isabelleは次を返します。

1. rev (rev xs) = xs

証明中は、証明すべき残りのサブゴールが示されます。もちろん最初は単にメインゴールが示されます。

再帰関数に関する証明は一般的に帰納法を使って証明します。今回は変数xsに関する帰納法で証明します。

apply(induct_tac xs)

複数のサブゴールがある場合、最初のサブゴールに適用されます。適用すると、基底ケース(Nil)と帰納ケース(Cons)の二つのサブゴールが得られます。

1. rev (rev []) = []
2. ∧a list. rev (rev list) = list ==> rev (rev (a # list)) = a # list

帰納ケースは典型的なサブゴールの形になっています。

i. ∧変数1 ... 変数n. 前提 ==> 結論

∧変数1 ... 変数n.の部分はとりあえず、このサブゴールのローカルな束縛変数と考えれば良いです。複数の前提がある場合は[| 前提1 ; 前提2 ; ... 前提n |]と書かれます。

両方のサブゴールの自動証明を試みます。

apply(auto)

autoは全てのサブゴールの自動証明を試みます。今回の場合、サブゴール1の証明は完了し、サブゴール2は次のサブゴールが残ります。

1. ∧a list. rev (rev list) = list ==> rev (rev list @ a # []) = a # list

このサブゴールから、補題が必要であることがわかります。

First Lemmas.

oopsで証明を中断するか、もしくはUndoで証明前の状態に戻り、次の補題の証明を行います。

lemma rev_app [simp]: "rev(xs @ ys) = (rev ys) @ (rev xs)"

lemmaとtheoremは単にそれが補題なのか定理なのかを示しており、全く同じ意味なのでどちらを使っても構いません。

今回の場合は変数が二つありますが、@の定義からxsを展開します。

apply(induct_tac xs)
apply(auto)

次のサブゴールが残ります。

1. rev ys = rev ys @ []

また別の補題が必要そうです。

Second Lemma.

lemma app_Nil2 [simp]: "xs @ [] = xs"
apply(induct_tac xs)
apply(auto)

全てのサブゴールの証明が終わると、IsabelleはNo subgoals!を返します。

xs @ [] = xs
No subgoals!

証明は次のようにして終了します。

done

doneの後は、定理と名前が結びつけられ参照可能となります。

最初の補題に戻って証明をやり直してみます。

lemma rev_app [simp]: "rev(xs @ ys) = (rev ys) @ (rev xs)"
apply(induct_tac xs)
apply(auto)

app_Nil2が自動的に使われ、先ほどより証明が進みますが、次のサブゴールが残ります。

1. ∧a list. rev (list @ ys) = rev ys @ rev list ==>
          (rev ys @ rev list) @ a # [] = rev ys @ rev list @ a # []

結論部分から、@の結合性に関するルールが足りないことがわかります。

Third Lemma.

@結合法則を証明します。

lemma app_assoc [simp]: "(xs @ ys) @ zs = xs @ (ys @ zs)"
apply(induct_tac xs)
apply(auto)
done

これで最初の補題も証明可能になりました。

lemma rev_app [simp]: "rev(xs @ ys) = (rev ys) @ (rev xs)"
apply(induct_tac xs)
apply(auto)
done

最後に、大本の定理の証明に戻ります。

theorem rev_rev [simp]: "rev (rev xs) = xs"
apply(induct_tac xs)
apply(auto)
done

証明できました。理論ファイルの最後はendで終えます。endでIsabelleのプロセスが終了することがわかると思います。

今回の補題には全て[simp]が付いていましたが、これを付けるかどうかは注意が必要です。自動証明の中でsimpルールが適用可能であれば左辺を右辺に変換していくので、変換のループがあると最悪の場合自動証明が無限ループになります。[simp]を付けなかった場合の証明の方法はこのあと登場します。

2.4 Some Helpful Commands

コマンドには証明を進めるためのコマンドと、補助的なコマンドがあります。証明を進める基本的なコマンドはapply(method)という形をしており、methodにはinduct_tacautoがあります。

補助コマンドを幾つか紹介します。

  • defer - 先頭のサブゴールを後回しにします。
  • prefer n - n番目のサブゴールを先頭にします。
  • thm 定理名 - 定理を表示します。
  • term 文字列 - 型チェックし、現在のコンテキストにおける項として表示します。
  • typ 文字列 - 現在のコンテキストにおける型として表示します。

2.5 Datatype

2.5.1 Lists

リストは計算機における基本的なデータ型です。HOLにはListという理論が定義されており、その中にはhd(head)、tl(tail)、mapfilterなどの関数も定義されています。また、[x1, ..., xn]x1 # ... # xn # []の省略形です。

2.5.2 The General Format

データ型の定義は次の形をしています。

datatype (型変数1, ..., 型変数n) 型名 = 構成子11 1 ... 型1 k1 | ... | 構成子mm 1 ... 型m km

構成子は大文字で始めるのが一般的です。

[] ~= x # xs(~は否定です)や(x # xs = y # ys) = (x = y & xs = ys)などの規則は、証明中の単純化のときに自動的に適用されます。これは原始再帰関数の定義でも同じです。

一部の例外を除くデータ型には関数sizeが定義されます。例えばリストの場合、sizeはリストの長さを返します。一般的には次の規則に従います。

  • 引数を取らない構成子の場合は0。
  • 引数を取る構成子の場合は、それぞれの引数のsizeの和に1を足した値。

sizeはそれぞれのデータ型に定義されている、つまりオーバーロードされています。リストの場合は、lengthという名前も付けられており、こちらはオーバーロードされていません。

2.5.3 Primitive Recursion

データ型上の関数は再帰関数、特に原始再帰関数で定義されることが多いです。つまり、再帰方程式は次の形をしていなければなりません。

f x1 ... (C y1 ... yk) ... xn = r

Cは構成子です。rf ... yi ...という形をしていなければなりません。これから、Isabelleはfが停止性を満たしていることを判断できます。構成子毎に多くとも1つの再帰方程式を定義することができ、その順番は任意です。

Exercise 2.5.1

二分木を定義します。

datatype 'a tree = Tip | Node "'a tree" 'a "'a tree"

サブツリーを再帰的に入れ替える関数mirrorを定義し、mirror (mirror t) = tを証明します。

primrec mirror :: "'a tree => 'a tree" where
"mirror Tip = Tip" |
"mirror (Node lt v rt) = Node (mirror rt) v (mirror lt)"

lemma "mirror_mirror: "mirror (mirror t) = t"
apply(induct_tac t)
apply(auto)
done

二分木を通りがけ順でリストに変換する関数flattenを定義し、flatten (mirror t) = rev (flatten t)を証明します。

primrec flatten :: "'a tree => 'a list" where
"flatten Tip = []" |
"flatten (Node lt v rt) = (flatten lt) @ (v # flatten rt)"

lemma "flatten (mirror t) = rev (flatten t)"
apply(induct_tac t)
apply(auto)
done
2.5.4 Case Expressions

次の例のように、データ型にcase式を使うこともできます。

case xs of [] => [] | y # ys => y

case式のパターン部分には関数型プログラムのように、データ型の構成子、変数、_(ワイルドカード)を使うことができます。

2.5.5 Structural Induction and Case Distinction

induct_tacによる展開は行き過ぎになる場合、case_tacによる場合分けが有効な場合があります。

lemma "(case xs of [] => [] | y # ys => xs) = xs"
apply(case_tac xs)

xsについて場合分けされ、二つのサブゴールが示されます。

1. xs= [] ==> (case xs of [] => [] | y # ys => xs) = xs
2. ∧a list. xs = a # list ==> (case xs of [] => [] | y # ys => xs) = xs

帰納法は前提に現れていない、自由変数または∧束縛変数にのみ適用できます。対して、case_tacは任意の項(変数とは限らない)に適用できます。ただし、∧以外で束縛されている変数を含んではなりません。

2.5.6 Case Study: Boolean Expressions

ブール式とその操作をどのようにモデル化し、Isabelleでどのように構成するか見ていきます。

Modelling Boolean Expressions.

ブール式は変数、定数、否定、論理積から構成することにします。

datatype boolex = Cosnt bool | Var nat | Neg boolex | And boolex boolex

例えばP0 ∧ ¬P1And (Var 0) (Neg (Var 1))となります。

The Value of a Boolean Expression.

ブール式の値は変数に依存するため、評価するにはnat => bool型の関数が環境として必要になります。評価関数を次のように定義します。

primrec "value" :: "boolex => (nat => bool) => bool" where
"value (Const b) env = b" |
"value (Var x)   env = env x" |
"value (Neg b)   env = (~ value b env)" |
"value (And b c) env = (value b env & value c env)"

関数名valueが"で括られているのは、コマンドvalueと区別するためです。

If-Expressions.

別の表現方法として、いわゆるif式があります。if式を定数(CIF)、変数(VIF)、条件文(IF)から定義します。

datatype ifex = CIF bool | VIF nat | IF ifex ifex ifex

if式の評価関数を定義します。

primrec valif :: "ifex => (nat => bool) => bool" where
"valif (CIF b)    env = b" |
"valif (VIF x)    env = env x" |
"valif (IF b t e) env = (if valif b env then valif t env
                                        else valif e env)"

Converting Boolean and If-Expressions.

boolexは一般的な論理式に近い表現であり、一方ifexは手続き的です。boolexからifexへの変換関数が定義できます。

primrec bool2if :: "boolex => ifex" where
"bool2if (Const b) = CIF b" |
"bool2if (Var x)   = VIF x" |
"bool2if (Neg b)   = IF (bool2if b) (CIF False) (CIF True)" |
"bool2if (And b c) = IF (bool2if b) (bool2if c) (CIF False)"

bool2ifが値を保存していることを証明します。

lemma "valif (bool2if b) env = value b env"
apply(induct_tac b)
apply(auto)
done

bool2ifを使ってboolexからifexへ変換しても、値が変わらないことが証明できました。

次に、if式を正規化してみます。正規形とは、IFの第一引数が変数または定数のみで、別のIFではないこととします。サブ関数とともに、正規化関数を次のように定義できます。

primrec normif :: "ifex => ifex => ifex => ifex" where
"normif (CIF b)    t e = IF (CIF b) t e" |
"normif (VIF x)    t e = IF (VIF x) t e" |
"normif (IF b t e) u f = normif b (normif t u f) (normif e u f)"

primrec norm :: "ifex => ifex" where
"norm (CIF b)    = CIF b" |
"norm (VIF x)    = VIF x" |
"norm (IF b t e) = normif b (norm t) (norm e)"

この定義は少し複雑ですが、Isabelleを使って値が変わっていないことを証明できます。

theorem "valif (norm b) env = valif b env"

これを証明するためには、次の補題が必要です。

lemma [simp]: "ALL t e. valif (normif b t e) env = valif (IF b t e) env"

次に、normが正しく正規化しているか確認します。そのために、正規化しているかどうか確認する関数を定義します。

primrec normal :: "ifex => bool" where
"normal (CIF b) = True" |
"normal (VIF x) = True" |
"normal (IF b t e) = (normal t & normal e &
                      (case b of CIF b => True | VIF x => True | IF x y z => False))"

これでnormal (norm b)を証明できれば、正しく正規化できていることがわかります。これを証明するには、次の補題が必要です。

lemma [simp]: "ALL t e. normal (normif b t e) = (normal t & normal e)"

メインゴールを自動証明した結果残ったサブゴールが、必要な補題のヒントとなります。補題に全称限量子が必要な理由は3.2節で説明します。

Exercise 2.5.2

正規化の定義を、IFの第一引数は変数でなければならないと変更してみます。

primrec normal :: "ifex => bool" where
"normal (CIF b)    = True" |
"normal (VIF x)    = True" |
"normal (IF b t e) = (normal t & normal e &
                      (case b of CIF b => False | VIF x => True | IF x y z => False))"

primrec normif :: "ifex => ifex => ifex => ifex" where
"normif (CIF b)    t e = (if b then t else e)" |
"normif (VIF x)    t e = IF (VIF x) t e" |
"normif (IF b t e) u f = normif b (normif t u f) (normif e u f)"

primrec norm :: "ifex => ifex" where
"norm (CIF b)    = CIF b" |
"norm (VIF x)    = VIF x" |
"norm (IF b t e) = normif b (norm t) (norm e)"

lemma [simp]: "ALL t e. valif (normif b t e) env = valif (IF b t e) env"
apply(induct_tac b)
apply(auto)
done

theorem "valif (norm b) env = valif b env"
apply(induct_tac b)
apply(auto)
done

lemma [simp]: "ALL t e. (normal t & normal e) --> normal (normif b t e)"
apply(induct_tac b)
apply(auto)
done

theorem "normal (norm b)"
apply(induct_tac b)
apply(auto)
done

-->は含意です。