関数を作る人と使う人のあいだで「何を渡せば、何が返るのか」を前もって決めておくと、誤りがどちらの側にあるのかがはっきりします。この記事は、その取り決め(契約)を数学の言葉で書き、プログラムが約束を守ることを証明する方法を学びます。読み終えると、ループの途中で成り立っていてほしい条件を見つけ、それを使ってプログラムの正しさと停止を示せるようになります。
モジュールの契約 は、呼び出し側が守る事前条件 P P P と、実装が守る事後条件 Q Q Q の組です。「P P P を満たす状態から実行して止まれば Q Q Q が成り立つ」という主張を Hoare の三つ組 { P } c { Q } \{P\}\ c\ \{Q\} { P } c { Q } と書きます。
代入・逐次・条件分岐・while・帰結の 5 つの規則(と操作の呼び出しの規則)で導出した三つ組は、実行の意味論に照らして正しい(健全性 )。while の規則が要求するのは、反復で保たれるループ不変条件 です。
不変条件は「止まれば正しい」ことしか保証しません。自然数に値をとり反復ごとに真に減る変量 があれば、ループは止まります。根拠は自然数の整列性です。
クラスの表現不変条件 は、構築子が成立させ、すべての公開操作が保ち、表現が外に漏れなければ、到達できるすべての状態で成り立ちます。表現を別名で渡すと、この帰納法の仮定が破れます。
下位型の実装が、事前条件を弱め(P ⇒ P ′ P \Rightarrow P' P ⇒ P ′ )事後条件を強める(Q ′ ⇒ Q Q' \Rightarrow Q Q ′ ⇒ Q )なら、上位型の契約だけで証明した呼び出し側は差し替えても正しいままです(Liskov の置換原則)。逆向きの変更は呼び出し側を壊します。
表明の実行時検査は、与えた入力での違反しか検出しません。すべての入力について契約が成り立つかの判定は、一般に決定不能です。テストは契約の有限個の点での検査、証明はすべての点での検査です。
この記事で使う事前知識
二分探索 : 昇順に並んだ列の真ん中を調べ、探索範囲を半分に絞っていく方法。→ 探索アルゴリズム
下限位置 : 昇順の列で、x x x 以上の値が最初に現れる位置のこと。→ 下限位置
配列 : 同じ種類の値を番号(添字)つきで並べたもの。→ 配列
プログラミング言語の意味 : プログラムの実行を、規則に従った書き換えとして数学的に定める考え方。→ プログラミング言語論
数学的帰納法 : 最初の場合と「ひとつ前が成り立てば次も成り立つ」ことから、すべての場合を示す証明法。
命題論理 : 「かつ」「または」「ならば」で条件を組み合わせて真偽を考える枠組み。
二分探索は 20 行に満たないアルゴリズムですが、探索アルゴリズム で見たとおり、正しい実装が公刊されるまでに長い年月がかかり、広く使われた標準ライブラリの中でも壊れていました。壊れていたのは算法の着想ではなく、「この関数は何を受け取り、何を返すと約束しているのか」「ループの途中で何が成り立っているのか」という、コードに書かれていない部分です。
プログラムの誤りを論じるには、まず「正しい」を定めなければなりません。関数 lower_bound(a, x) が正しいかどうかは、a が昇順であるという前提を置くかどうかで変わります。昇順でない配列 を渡して変な値が返ってきたとき、誤っているのは関数ではなく呼び出し側です。逆に、昇順の配列を渡して下限位置でない値が返ってきたら、誤っているのは関数です。責任の境界 を決めるのが契約です。B. Meyer はこの考え方を「契約による設計」(Design by Contract)と名づけ、事前条件を呼び出し側の義務、事後条件を実装側の義務として、クラスのインターフェースに書き込むことを提案しました。
契約を書くだけなら注釈にすぎません。この記事の主題は、契約から何が証明できるか です。C. A. R. Hoare は 1969 年の論文で、プログラムの各構文に対応する推論規則を与え、契約をプログラムの構造に沿って機械的に導出できることを示しました。この体系(Hoare 論理)の規則が正しいこと、つまり導出した契約が実際の実行で必ず守られることを、この記事では実行の意味論から証明します。そのうえで、同じ考え方をクラスの不変条件と、下位クラスへの差し替えに広げます。
この章は、テーマ「ソフトウェア設計とテスト」の出発点です。次章以降のテストは、いつも「何かに対する」テストです。その「何か」がこの章の契約で、テストは契約を有限個の入力で検査し、証明はすべての入力で検査します。両者の違いは最後の節で命題の形にします。
定義 2.1 (仕様・実装・インターフェース ) ↗
入力の集合 X X X と出力の集合 Y Y Y を固定する。
仕様 とは、関係 S ⊆ X × Y S \subseteq X \times Y S ⊆ X × Y と部分集合 D ⊆ X D \subseteq X D ⊆ X (仕様が何かを約束する入力の範囲)の組 ( D , S ) (D, S) ( D , S ) をいう。( x , y ) ∈ S (x, y) \in S ( x , y ) ∈ S は「入力 x x x に対して出力 y y y は許される」ことを表す。
実装 とは、X X X から Y Y Y への部分関数 f f f (止まらない入力では値が定まらない)をいう。
実装 f f f が仕様 ( D , S ) (D, S) ( D , S ) を満たす とは、すべての x ∈ D x \in D x ∈ D について f ( x ) f(x) f ( x ) が定まり、( x , f ( x ) ) ∈ S (x, f(x)) \in S ( x , f ( x )) ∈ S であることをいう。
モジュール(関数・クラス・パッケージ)が外に見せる操作の名前と、その仕様の集まりをインターフェース という。インターフェースに現れない実装の詳細(データの持ち方、補助関数)を外から使えないようにすることを情報隠蔽 という。
仕様が関係であって関数でないのは、許される出力が 1 つとは限らないからです。「配列の中の x x x の位置を 1 つ返す」という仕様は、x x x が複数あればどれを返してもよいとしています。D D D の外の入力について、仕様は何も約束しません。実装は何を返しても、止まらなくても、仕様に違反しません。
情報隠蔽は、D. Parnas が 1972 年に、モジュール分割の基準として提案した考え方です。隠す理由は秘密にしたいからではなく、隠したものは後から変えられる からです。呼び出し側がインターフェースの仕様だけを使って書かれていれば、実装を別のもの(同じ仕様を満たすもの)に取り替えても呼び出し側は正しいままです。この「取り替えても正しい」を定理にしたものが、第 7 節の 定理 7.2 です。
定義 2.2 (契約:事前条件と事後条件 ) ↗
状態(変数の値の割り当て)の集合を Σ \Sigma Σ とする。Σ \Sigma Σ の部分集合を表明 という。表明 P P P と状態 σ \sigma σ について、σ ∈ P \sigma \in P σ ∈ P を P ( σ ) P(\sigma) P ( σ ) とも書き、「σ \sigma σ で P P P が成り立つ」と読む。
操作 m m m の契約 とは、表明の組 ( P , Q ) (P, Q) ( P , Q ) をいう。P P P を事前条件 、Q Q Q を事後条件 という。契約 ( P , Q ) (P, Q) ( P , Q ) は次の約束を表す。
呼び出し側の義務 : P P P が成り立つ状態でだけ m m m を呼ぶ。
実装側の義務 : P P P が成り立つ状態から実行を始めて止まったなら、止まった状態で Q Q Q が成り立つ。
表明 P , P ′ P, P' P , P ′ について、P ⊆ P ′ P \subseteq P' P ⊆ P ′ のとき P ⇒ P ′ P \Rightarrow P' P ⇒ P ′ と書き、P P P は P ′ P' P ′ より強い 、P ′ P' P ′ は P P P より弱い という。P ∧ P ′ = P ∩ P ′ P \wedge P' = P \cap P' P ∧ P ′ = P ∩ P ′ 、P ∨ P ′ = P ∪ P ′ P \vee P' = P \cup P' P ∨ P ′ = P ∪ P ′ 、¬ P = Σ ∖ P \lnot P = \Sigma \setminus P ¬ P = Σ ∖ P 、t r u e = Σ \mathrm{true} = \Sigma true = Σ 、f a l s e = ∅ \mathrm{false} = \emptyset false = ∅ と書く。
表明を論理式ではなく状態の集合として定めたのは、論理式の言語を固定する手間を省くためです。「x ≥ 0 x \ge 0 x ≥ 0 」と書いたら、それは { σ ∣ σ ( x ) ≥ 0 } \{\sigma \mid \sigma(x) \ge 0\} { σ ∣ σ ( x ) ≥ 0 } という集合のことです。強い・弱いの向きに注意してください。強い条件ほど、満たす状態が少ない 。事前条件が弱いほど実装は多くの入力を引き受け、事後条件が強いほど実装は多くを約束します。f a l s e \mathrm{false} false はもっとも強く、t r u e \mathrm{true} true はもっとも弱い表明です。
小さな例で確かめます。命令「x x x に 1 を足す」を、x ≥ 0 x \ge 0 x ≥ 0 の状態から実行すると、終わった状態では x ≥ 1 x \ge 1 x ≥ 1 です。事前条件を x ≥ 0 x \ge 0 x ≥ 0 、事後条件を x ≥ 1 x \ge 1 x ≥ 1 とする契約が、この命令について成り立ちます。また「x ≥ 5 x \ge 5 x ≥ 5 」を満たす状態は必ず「x ≥ 0 x \ge 0 x ≥ 0 」も満たすので、x ≥ 5 x \ge 5 x ≥ 5 は x ≥ 0 x \ge 0 x ≥ 0 より強い条件です。
状態全体 Σ 弱い条件 P’ 強い条件 P
強い条件ほど、満たす状態の集合は小さい。P は P' より強く、P ならば P' が成り立つ。
事後条件が「実行前の値」に触れたいことがあります(「幅を変えても高さは変わらない」)。この記事では、実行前の値を表す定数 h 0 h_0 h 0 ごとに契約 ( P ∧ h = h 0 , Q ∧ h = h 0 ) (P \wedge h = h_0,\ Q \wedge h = h_0) ( P ∧ h = h 0 , Q ∧ h = h 0 ) を 1 つずつ用意し、契約の族 として表します。定義 3.7 の契約表が三つ組の集合になっているのは、そのためです。
契約が「守られる」とはどういうことかを定めるには、プログラムの実行を数学的に定めなければなりません。プログラミング言語論 の言語 L(定義 4.1[プログラミング言語論] )は、式を 1 ステップずつ書き換える簡約関係で意味を定めました。ここでは変数への代入とループをもつ命令型の言語 W を用意し、「命令 c c c を状態 σ \sigma σ から実行すると状態 σ ′ \sigma' σ ′ で止まる」という関係を直接定めます(大ステップ意味論)。
次の定義は長く見えますが、やっていることは「Python のような小さな言語の、各構文が何をするか」を一覧にするだけです。横線の書き方は「線の上が成り立てば、線の下も成り立つ」と読みます。たとえば「x : = x + 1 x := x + 1 x := x + 1 を、x = 3 x = 3 x = 3 の状態から実行すると、x = 4 x = 4 x = 4 の状態で終わる」ことが、この関係の最も簡単な例です。まず規則の表は流し読みし、第 3.2 節以降で必要になったときに戻ってください。
定義 3.1 (言語 W ) ↗
V a r \mathrm{Var} Var を変数の集合、O p \mathrm{Op} Op を操作名の有限集合とする。状態 とは写像 σ : V a r → Z \sigma : \mathrm{Var} \to \mathbb{Z} σ : Var → Z をいい、状態の全体を Σ \Sigma Σ と書く。σ [ x ↦ n ] \sigma[x \mapsto n] σ [ x ↦ n ] は、x x x の値を n n n に変え、ほかの変数の値を σ \sigma σ と同じにした状態を表す。
算術式 a a a 、ブール式 b b b 、命令 c c c を次の文法で定める(n ∈ Z n \in \mathbb{Z} n ∈ Z 、x ∈ V a r x \in \mathrm{Var} x ∈ Var 、k k k は正の整数、m ∈ O p m \in \mathrm{Op} m ∈ Op )。
a : : = n ∣ x ∣ a 1 + a 2 ∣ a 1 − a 2 ∣ a 1 × a 2 ∣ a 1 ÷ k b : : = t r u e ∣ a 1 = a 2 ∣ a 1 ≤ a 2 ∣ ¬ b ∣ b 1 ∧ b 2 c : : = s k i p ∣ x : = a ∣ c 1 ; c 2 ∣ i f b t h e n c 1 e l s e c 2 ∣ w h i l e b d o c ∣ c a l l m \begin{aligned}
a &::= n \mid x \mid a_1 + a_2 \mid a_1 - a_2 \mid a_1 \times a_2 \mid a_1 \div k \\
b &::= \mathtt{true} \mid a_1 = a_2 \mid a_1 \le a_2 \mid \lnot b \mid b_1 \wedge b_2 \\
c &::= \mathtt{skip} \mid x := a \mid c_1 ; c_2 \mid \mathtt{if}\ b\ \mathtt{then}\ c_1\ \mathtt{else}\ c_2 \mid \mathtt{while}\ b\ \mathtt{do}\ c \mid \mathtt{call}\ m
\end{aligned} a b c ::= n ∣ x ∣ a 1 + a 2 ∣ a 1 − a 2 ∣ a 1 × a 2 ∣ a 1 ÷ k ::= true ∣ a 1 = a 2 ∣ a 1 ≤ a 2 ∣ ¬ b ∣ b 1 ∧ b 2 ::= skip ∣ x := a ∣ c 1 ; c 2 ∣ if b then c 1 else c 2 ∣ while b do c ∣ call m 式の値 ⟦ a ⟧ σ ∈ Z \llbracket a \rrbracket\sigma \in \mathbb{Z} [ [ a ] ] σ ∈ Z 、⟦ b ⟧ σ ∈ { t t , f f } \llbracket b \rrbracket\sigma \in \{\mathrm{tt}, \mathrm{ff}\} [ [ b ] ] σ ∈ { tt , ff } を、式の構造についての再帰で定める。⟦ n ⟧ σ = n \llbracket n \rrbracket\sigma = n [ [ n ] ] σ = n 、⟦ x ⟧ σ = σ ( x ) \llbracket x \rrbracket\sigma = \sigma(x) [ [ x ] ] σ = σ ( x ) 、⟦ a 1 + a 2 ⟧ σ = ⟦ a 1 ⟧ σ + ⟦ a 2 ⟧ σ \llbracket a_1 + a_2 \rrbracket\sigma = \llbracket a_1 \rrbracket\sigma + \llbracket a_2 \rrbracket\sigma [ [ a 1 + a 2 ] ] σ = [ [ a 1 ] ] σ + [ [ a 2 ] ] σ (− - − 、× \times × も同様)、⟦ a 1 ÷ k ⟧ σ = ⌊ ⟦ a 1 ⟧ σ / k ⌋ \llbracket a_1 \div k \rrbracket\sigma = \lfloor \llbracket a_1 \rrbracket\sigma / k \rfloor [ [ a 1 ÷ k ] ] σ = ⌊ [ [ a 1 ] ] σ / k ⌋ 。⟦ t r u e ⟧ σ = t t \llbracket \mathtt{true} \rrbracket\sigma = \mathrm{tt} [ [ true ] ] σ = tt 、⟦ a 1 = a 2 ⟧ σ = t t \llbracket a_1 = a_2 \rrbracket\sigma = \mathrm{tt} [ [ a 1 = a 2 ] ] σ = tt となるのは ⟦ a 1 ⟧ σ = ⟦ a 2 ⟧ σ \llbracket a_1 \rrbracket\sigma = \llbracket a_2 \rrbracket\sigma [ [ a 1 ] ] σ = [ [ a 2 ] ] σ のとき(≤ \le ≤ も同様)、¬ \lnot ¬ と ∧ \wedge ∧ は真理値の否定と連言。ブール式 b b b を表明 { σ ∣ ⟦ b ⟧ σ = t t } \{\sigma \mid \llbracket b \rrbracket\sigma = \mathrm{tt}\} { σ ∣ [ [ b ] ] σ = tt } と同一視する。a 1 < a 2 a_1 \lt a_2 a 1 < a 2 は ¬ ( a 2 ≤ a 1 ) \lnot(a_2 \le a_1) ¬ ( a 2 ≤ a 1 ) の略記とする。
解釈 M \mathcal{M} M とは、各操作名 m ∈ O p m \in \mathrm{Op} m ∈ Op に関係 M ( m ) ⊆ Σ × Σ \mathcal{M}(m) \subseteq \Sigma \times \Sigma M ( m ) ⊆ Σ × Σ を割り当てるものをいう。解釈 M \mathcal{M} M のもとでの実行関係 ⟨ c , σ ⟩ ⇓ σ ′ \langle c, \sigma \rangle \Downarrow \sigma' ⟨ c , σ ⟩ ⇓ σ ′ を、次の規則で生成される最小の関係とする。
(Skip) ⟨ s k i p , σ ⟩ ⇓ σ (Assign) ⟨ x : = a , σ ⟩ ⇓ σ [ x ↦ ⟦ a ⟧ σ ] (Seq) ⟨ c 1 , σ ⟩ ⇓ σ 1 ⟨ c 2 , σ 1 ⟩ ⇓ σ ′ ⟨ c 1 ; c 2 , σ ⟩ ⇓ σ ′ (Call) ( σ , σ ′ ) ∈ M ( m ) ⟨ c a l l m , σ ⟩ ⇓ σ ′ (IfT) ⟦ b ⟧ σ = t t ⟨ c 1 , σ ⟩ ⇓ σ ′ ⟨ i f b t h e n c 1 e l s e c 2 , σ ⟩ ⇓ σ ′ (IfF) ⟦ b ⟧ σ = f f ⟨ c 2 , σ ⟩ ⇓ σ ′ ⟨ i f b t h e n c 1 e l s e c 2 , σ ⟩ ⇓ σ ′ (WhF) ⟦ b ⟧ σ = f f ⟨ w h i l e b d o c , σ ⟩ ⇓ σ (WhT) ⟦ b ⟧ σ = t t ⟨ c , σ ⟩ ⇓ σ 1 ⟨ w h i l e b d o c , σ 1 ⟩ ⇓ σ ′ ⟨ w h i l e b d o c , σ ⟩ ⇓ σ ′ \begin{aligned}
&\text{(Skip)} && \langle \mathtt{skip}, \sigma \rangle \Downarrow \sigma
\qquad\qquad \text{(Assign)} \quad \langle x := a, \sigma \rangle \Downarrow \sigma[x \mapsto \llbracket a \rrbracket\sigma] \\[4pt]
&\text{(Seq)} && \frac{\langle c_1, \sigma \rangle \Downarrow \sigma_1 \qquad \langle c_2, \sigma_1 \rangle \Downarrow \sigma'}{\langle c_1 ; c_2, \sigma \rangle \Downarrow \sigma'}
\qquad\qquad \text{(Call)} \quad \frac{(\sigma, \sigma') \in \mathcal{M}(m)}{\langle \mathtt{call}\ m, \sigma \rangle \Downarrow \sigma'} \\[4pt]
&\text{(IfT)} && \frac{\llbracket b \rrbracket\sigma = \mathrm{tt} \qquad \langle c_1, \sigma \rangle \Downarrow \sigma'}{\langle \mathtt{if}\ b\ \mathtt{then}\ c_1\ \mathtt{else}\ c_2, \sigma \rangle \Downarrow \sigma'}
\qquad \text{(IfF)} \quad \frac{\llbracket b \rrbracket\sigma = \mathrm{ff} \qquad \langle c_2, \sigma \rangle \Downarrow \sigma'}{\langle \mathtt{if}\ b\ \mathtt{then}\ c_1\ \mathtt{else}\ c_2, \sigma \rangle \Downarrow \sigma'} \\[4pt]
&\text{(WhF)} && \frac{\llbracket b \rrbracket\sigma = \mathrm{ff}}{\langle \mathtt{while}\ b\ \mathtt{do}\ c, \sigma \rangle \Downarrow \sigma}
\qquad\qquad \text{(WhT)} \quad \frac{\llbracket b \rrbracket\sigma = \mathrm{tt} \qquad \langle c, \sigma \rangle \Downarrow \sigma_1 \qquad \langle \mathtt{while}\ b\ \mathtt{do}\ c, \sigma_1 \rangle \Downarrow \sigma'}{\langle \mathtt{while}\ b\ \mathtt{do}\ c, \sigma \rangle \Downarrow \sigma'}
\end{aligned} (Skip) (Seq) (IfT) (WhF) ⟨ skip , σ ⟩ ⇓ σ (Assign) ⟨ x := a , σ ⟩ ⇓ σ [ x ↦ [ [ a ] ] σ ] ⟨ c 1 ; c 2 , σ ⟩ ⇓ σ ′ ⟨ c 1 , σ ⟩ ⇓ σ 1 ⟨ c 2 , σ 1 ⟩ ⇓ σ ′ (Call) ⟨ call m , σ ⟩ ⇓ σ ′ ( σ , σ ′ ) ∈ M ( m ) ⟨ if b then c 1 else c 2 , σ ⟩ ⇓ σ ′ [ [ b ] ] σ = tt ⟨ c 1 , σ ⟩ ⇓ σ ′ (IfF) ⟨ if b then c 1 else c 2 , σ ⟩ ⇓ σ ′ [ [ b ] ] σ = ff ⟨ c 2 , σ ⟩ ⇓ σ ′ ⟨ while b do c , σ ⟩ ⇓ σ [ [ b ] ] σ = ff (WhT) ⟨ while b do c , σ ⟩ ⇓ σ ′ [ [ b ] ] σ = tt ⟨ c , σ ⟩ ⇓ σ 1 ⟨ while b do c , σ 1 ⟩ ⇓ σ ′ ⟨ c , σ ⟩ ⇓ σ ′ \langle c, \sigma \rangle \Downarrow \sigma' ⟨ c , σ ⟩ ⇓ σ ′ となる σ ′ \sigma' σ ′ が存在するとき、c c c は σ \sigma σ から停止する という。
「規則で生成される最小の関係」とは、規則を有限回使って組み立てた導出 (有限の木)があるものだけが関係に属する、ということです。導出の高さ は自然数なので、「導出についての帰納法」は高さについての完全帰納法(定理 4.3[証明の技術] )として正当化されます。w h i l e t r u e d o s k i p \mathtt{while}\ \mathtt{true}\ \mathtt{do}\ \mathtt{skip} while true do skip にはどの状態からも有限の導出がありません。(WhT) を使うたびに同じ命令の導出がもう 1 つ必要になるからです。止まらない実行は、この意味論では「⇓ \Downarrow ⇓ が成り立つ σ ′ \sigma' σ ′ が無い」として表されます。
式の値は、状態の全域関数として定めました(ゼロ除算を避けるため、割り算は正の定数による切り捨て除算だけにしています)。以下の証明が式について使うのは、この「状態が決まれば値が決まる」ことだけです。c a l l m \mathtt{call}\ m call m は、中身を書かない操作の呼び出しです。中身の代わりに解釈 M \mathcal{M} M が状態の変化を与えます。第 7 節で、M \mathcal{M} M を取り替えることが「実装の差し替え」になります。
補題 3.2 (実行の決定性 ) ✓
解釈 M \mathcal{M} M がすべての m ∈ O p m \in \mathrm{Op} m ∈ Op について部分関数である(( σ , σ 1 ) , ( σ , σ 2 ) ∈ M ( m ) (\sigma, \sigma_1), (\sigma, \sigma_2) \in \mathcal{M}(m) ( σ , σ 1 ) , ( σ , σ 2 ) ∈ M ( m ) ならば σ 1 = σ 2 \sigma_1 = \sigma_2 σ 1 = σ 2 )とする。このとき、⟨ c , σ ⟩ ⇓ σ ′ \langle c, \sigma \rangle \Downarrow \sigma' ⟨ c , σ ⟩ ⇓ σ ′ かつ ⟨ c , σ ⟩ ⇓ σ ′ ′ \langle c, \sigma \rangle \Downarrow \sigma'' ⟨ c , σ ⟩ ⇓ σ ′′ ならば σ ′ = σ ′ ′ \sigma' = \sigma'' σ ′ = σ ′′ である。
証明(補題 3.2) 言語 L の決定性(定理 4.4[プログラミング言語論] )と同じ方針で、⟨ c , σ ⟩ ⇓ σ ′ \langle c, \sigma \rangle \Downarrow \sigma' ⟨ c , σ ⟩ ⇓ σ ′ の導出の高さについての完全帰納法(定理 4.3[証明の技術] )で示します。導出の最後の規則で場合を分けます。
(Skip)。c = s k i p c = \mathtt{skip} c = skip で、s k i p \mathtt{skip} skip を結論にもつ規則は (Skip) だけなので、σ ′ = σ = σ ′ ′ \sigma' = \sigma = \sigma'' σ ′ = σ = σ ′′ です。
(Assign)。c c c は x : = a x := a x := a で、この形を結論にもつ規則は (Assign) だけです。⟦ a ⟧ σ \llbracket a \rrbracket\sigma [ [ a ] ] σ は σ \sigma σ から 1 つに決まるので、σ ′ = σ [ x ↦ ⟦ a ⟧ σ ] = σ ′ ′ \sigma' = \sigma[x \mapsto \llbracket a \rrbracket\sigma] = \sigma'' σ ′ = σ [ x ↦ [ [ a ] ] σ ] = σ ′′ です。
(Call)。c = c a l l m c = \mathtt{call}\ m c = call m で、適用できる規則は (Call) だけです。( σ , σ ′ ) , ( σ , σ ′ ′ ) ∈ M ( m ) (\sigma, \sigma'), (\sigma, \sigma'') \in \mathcal{M}(m) ( σ , σ ′ ) , ( σ , σ ′′ ) ∈ M ( m ) なので、M ( m ) \mathcal{M}(m) M ( m ) が部分関数であるという仮定から σ ′ = σ ′ ′ \sigma' = \sigma'' σ ′ = σ ′′ です。
(Seq)。c = c 1 ; c 2 c = c_1; c_2 c = c 1 ; c 2 で、前提は ⟨ c 1 , σ ⟩ ⇓ σ 1 \langle c_1, \sigma \rangle \Downarrow \sigma_1 ⟨ c 1 , σ ⟩ ⇓ σ 1 と ⟨ c 2 , σ 1 ⟩ ⇓ σ ′ \langle c_2, \sigma_1 \rangle \Downarrow \sigma' ⟨ c 2 , σ 1 ⟩ ⇓ σ ′ です。もう一方の導出も (Seq) で終わり、前提は ⟨ c 1 , σ ⟩ ⇓ σ 1 ′ \langle c_1, \sigma \rangle \Downarrow \sigma_1' ⟨ c 1 , σ ⟩ ⇓ σ 1 ′ と ⟨ c 2 , σ 1 ′ ⟩ ⇓ σ ′ ′ \langle c_2, \sigma_1' \rangle \Downarrow \sigma'' ⟨ c 2 , σ 1 ′ ⟩ ⇓ σ ′′ です。⟨ c 1 , σ ⟩ ⇓ σ 1 \langle c_1, \sigma \rangle \Downarrow \sigma_1 ⟨ c 1 , σ ⟩ ⇓ σ 1 の導出は元の導出より低いので、帰納法の仮定から σ 1 = σ 1 ′ \sigma_1 = \sigma_1' σ 1 = σ 1 ′ 。すると ⟨ c 2 , σ 1 ⟩ ⇓ σ ′ \langle c_2, \sigma_1 \rangle \Downarrow \sigma' ⟨ c 2 , σ 1 ⟩ ⇓ σ ′ と ⟨ c 2 , σ 1 ⟩ ⇓ σ ′ ′ \langle c_2, \sigma_1 \rangle \Downarrow \sigma'' ⟨ c 2 , σ 1 ⟩ ⇓ σ ′′ にもう一度帰納法の仮定を使えて、σ ′ = σ ′ ′ \sigma' = \sigma'' σ ′ = σ ′′ です。
(IfT)。⟦ b ⟧ σ = t t \llbracket b \rrbracket\sigma = \mathrm{tt} [ [ b ] ] σ = tt なので、もう一方の導出の最後の規則は (IfF) ではありえず((IfF) は ⟦ b ⟧ σ = f f \llbracket b \rrbracket\sigma = \mathrm{ff} [ [ b ] ] σ = ff を要求します)、(IfT) です。どちらの前提も c 1 c_1 c 1 を σ \sigma σ から実行したもので、帰納法の仮定から σ ′ = σ ′ ′ \sigma' = \sigma'' σ ′ = σ ′′ です。(IfF) も同じです。
(WhF)。⟦ b ⟧ σ = f f \llbracket b \rrbracket\sigma = \mathrm{ff} [ [ b ] ] σ = ff なので、もう一方の導出も (WhF) で終わり、σ ′ = σ = σ ′ ′ \sigma' = \sigma = \sigma'' σ ′ = σ = σ ′′ です。
(WhT)。⟦ b ⟧ σ = t t \llbracket b \rrbracket\sigma = \mathrm{tt} [ [ b ] ] σ = tt なので、もう一方も (WhT) で終わります。前提は ⟨ c , σ ⟩ ⇓ σ 1 \langle c, \sigma \rangle \Downarrow \sigma_1 ⟨ c , σ ⟩ ⇓ σ 1 、⟨ w h i l e b d o c , σ 1 ⟩ ⇓ σ ′ \langle \mathtt{while}\ b\ \mathtt{do}\ c, \sigma_1 \rangle \Downarrow \sigma' ⟨ while b do c , σ 1 ⟩ ⇓ σ ′ と、⟨ c , σ ⟩ ⇓ σ 1 ′ \langle c, \sigma \rangle \Downarrow \sigma_1' ⟨ c , σ ⟩ ⇓ σ 1 ′ 、⟨ w h i l e b d o c , σ 1 ′ ⟩ ⇓ σ ′ ′ \langle \mathtt{while}\ b\ \mathtt{do}\ c, \sigma_1' \rangle \Downarrow \sigma'' ⟨ while b do c , σ 1 ′ ⟩ ⇓ σ ′′ です。帰納法の仮定を本体の実行に使って σ 1 = σ 1 ′ \sigma_1 = \sigma_1' σ 1 = σ 1 ′ 、続いて残りのループの実行(これも元の導出より低い)に使って σ ′ = σ ′ ′ \sigma' = \sigma'' σ ′ = σ ′′ です。
∎
定義 3.3 (Hoare の三つ組:部分正当性と全正当性 ) ↗
P , Q P, Q P , Q を表明、c c c を言語 W の命令、M \mathcal{M} M を解釈とする。
三つ組 { P } c { Q } \{P\}\ c\ \{Q\} { P } c { Q } が M \mathcal{M} M のもとで部分正当 であるとは、すべての状態 σ , σ ′ \sigma, \sigma' σ , σ ′ について「P ( σ ) P(\sigma) P ( σ ) かつ ⟨ c , σ ⟩ ⇓ σ ′ \langle c, \sigma \rangle \Downarrow \sigma' ⟨ c , σ ⟩ ⇓ σ ′ ならば Q ( σ ′ ) Q(\sigma') Q ( σ ′ ) 」が成り立つことをいう。このとき ⊨ M { P } c { Q } \models_{\mathcal{M}} \{P\}\ c\ \{Q\} ⊨ M { P } c { Q } と書く。
三つ組 [ P ] c [ Q ] [P]\ c\ [Q] [ P ] c [ Q ] が M \mathcal{M} M のもとで全正当 であるとは、すべての状態 σ \sigma σ について「P ( σ ) P(\sigma) P ( σ ) ならば、⟨ c , σ ⟩ ⇓ σ ′ \langle c, \sigma \rangle \Downarrow \sigma' ⟨ c , σ ⟩ ⇓ σ ′ かつ Q ( σ ′ ) Q(\sigma') Q ( σ ′ ) を満たす σ ′ \sigma' σ ′ が存在する」が成り立つことをいう。このとき ⊨ M [ P ] c [ Q ] \models_{\mathcal{M}} [P]\ c\ [Q] ⊨ M [ P ] c [ Q ] と書く。
部分正当性は「止まれば正しい」、全正当性は「止まって、しかも正しい」です。止まらないプログラムは、どんな事後条件についても部分正当です。たとえば ⊨ { t r u e } w h i l e t r u e d o s k i p { f a l s e } \models \{\mathrm{true}\}\ \mathtt{while}\ \mathtt{true}\ \mathtt{do}\ \mathtt{skip}\ \{\mathrm{false}\} ⊨ { true } while true do skip { false } が成り立ちます。前提「⟨ c , σ ⟩ ⇓ σ ′ \langle c, \sigma \rangle \Downarrow \sigma' ⟨ c , σ ⟩ ⇓ σ ′ 」を満たす σ ′ \sigma' σ ′ が無いからです。部分正当性だけでは弱すぎるように見えますが、正しさと停止性を分けて証明できることが利点で、停止性は第 5 節で別に扱います。
代入の規則を述べるには、表明の中の変数を式で置き換える操作が要ります。
定義 3.4 (表明への代入 ) ↗
表明 Q Q Q 、変数 x x x 、算術式 a a a に対して、表明 Q [ a / x ] Q[a/x] Q [ a / x ] を
Q [ a / x ] = { σ ∈ Σ ∣ Q ( σ [ x ↦ ⟦ a ⟧ σ ] ) } Q[a/x] = \{\, \sigma \in \Sigma \mid Q(\sigma[x \mapsto \llbracket a \rrbracket\sigma]) \,\} Q [ a / x ] = { σ ∈ Σ ∣ Q ( σ [ x ↦ [ [ a ] ] σ ]) } で定める。また、算術式 a ′ a' a ′ とブール式 b b b の中の変数 x x x をすべて a a a に書き換えた式を a ′ [ a / x ] a'[a/x] a ′ [ a / x ] 、b [ a / x ] b[a/x] b [ a / x ] と書く。
Q [ a / x ] Q[a/x] Q [ a / x ] は「x x x に a a a の値を入れたあとで Q Q Q が成り立つような、入れる前の状態」の集合です。Q Q Q が式で書かれているとき、この集合は文字どおりの書き換えで得られます。それが次の補題です。この補題は、代入の規則(次の命題)を実際の式の書き換えで計算できるようにするための準備です。証明は、式の作りに沿って場合を分けるだけの確認作業です。
補題 3.5 (置換補題 ) ✓
算術式 a , a ′ a, a' a , a ′ 、ブール式 b b b 、変数 x x x 、状態 σ \sigma σ について
⟦ a ′ [ a / x ] ⟧ σ = ⟦ a ′ ⟧ ( σ [ x ↦ ⟦ a ⟧ σ ] ) , ⟦ b [ a / x ] ⟧ σ = ⟦ b ⟧ ( σ [ x ↦ ⟦ a ⟧ σ ] ) \llbracket a'[a/x] \rrbracket\sigma = \llbracket a' \rrbracket\bigl(\sigma[x \mapsto \llbracket a \rrbracket\sigma]\bigr), \qquad
\llbracket b[a/x] \rrbracket\sigma = \llbracket b \rrbracket\bigl(\sigma[x \mapsto \llbracket a \rrbracket\sigma]\bigr) [ [ a ′ [ a / x ] ] ] σ = [ [ a ′ ] ] ( σ [ x ↦ [ [ a ] ] σ ] ) , [ [ b [ a / x ] ] ] σ = [ [ b ] ] ( σ [ x ↦ [ [ a ] ] σ ] ) が成り立つ。とくに、ブール式 b b b を表明とみたとき、式の書き換え b [ a / x ] b[a/x] b [ a / x ] が表す表明は 定義 3.4 の b [ a / x ] b[a/x] b [ a / x ] と一致する。
証明(補題 3.5) τ = σ [ x ↦ ⟦ a ⟧ σ ] \tau = \sigma[x \mapsto \llbracket a \rrbracket\sigma] τ = σ [ x ↦ [ [ a ] ] σ ] とおきます。まず算術式 a ′ a' a ′ の構造についての帰納法で第 1 式を示します。
a ′ = n a' = n a ′ = n (定数)のとき、a ′ [ a / x ] = n a'[a/x] = n a ′ [ a / x ] = n で、両辺とも n n n です。a ′ = x a' = x a ′ = x のとき、a ′ [ a / x ] = a a'[a/x] = a a ′ [ a / x ] = a なので左辺は ⟦ a ⟧ σ \llbracket a \rrbracket\sigma [ [ a ] ] σ 、右辺は τ ( x ) = ⟦ a ⟧ σ \tau(x) = \llbracket a \rrbracket\sigma τ ( x ) = [ [ a ] ] σ です。a ′ = y a' = y a ′ = y (x x x と異なる変数)のとき、a ′ [ a / x ] = y a'[a/x] = y a ′ [ a / x ] = y なので左辺は σ ( y ) \sigma(y) σ ( y ) 、右辺は τ ( y ) = σ ( y ) \tau(y) = \sigma(y) τ ( y ) = σ ( y ) です(τ \tau τ は x x x 以外で σ \sigma σ と同じ)。a ′ = a 1 + a 2 a' = a_1 + a_2 a ′ = a 1 + a 2 のとき、書き換えは部分式ごとに行うので a ′ [ a / x ] = a 1 [ a / x ] + a 2 [ a / x ] a'[a/x] = a_1[a/x] + a_2[a/x] a ′ [ a / x ] = a 1 [ a / x ] + a 2 [ a / x ] で、
⟦ a ′ [ a / x ] ⟧ σ = ⟦ a 1 [ a / x ] ⟧ σ + ⟦ a 2 [ a / x ] ⟧ σ = ⟦ a 1 ⟧ τ + ⟦ a 2 ⟧ τ = ⟦ a 1 + a 2 ⟧ τ \llbracket a'[a/x] \rrbracket\sigma = \llbracket a_1[a/x] \rrbracket\sigma + \llbracket a_2[a/x] \rrbracket\sigma = \llbracket a_1 \rrbracket\tau + \llbracket a_2 \rrbracket\tau = \llbracket a_1 + a_2 \rrbracket\tau [ [ a ′ [ a / x ] ] ] σ = [ [ a 1 [ a / x ] ] ] σ + [ [ a 2 [ a / x ] ] ] σ = [ [ a 1 ] ] τ + [ [ a 2 ] ] τ = [ [ a 1 + a 2 ] ] τ です(2 つ目の等号が帰納法の仮定)。− - − 、× \times × 、÷ k \div k ÷ k も、値の定義が部分式の値の関数なので同じです。
ブール式についても構造についての帰納法で示します。b = t r u e b = \mathtt{true} b = true は両辺 t t \mathrm{tt} tt 。b = ( a 1 = a 2 ) b = (a_1 = a_2) b = ( a 1 = a 2 ) のとき、⟦ b [ a / x ] ⟧ σ = t t \llbracket b[a/x] \rrbracket\sigma = \mathrm{tt} [ [ b [ a / x ] ] ] σ = tt は ⟦ a 1 [ a / x ] ⟧ σ = ⟦ a 2 [ a / x ] ⟧ σ \llbracket a_1[a/x] \rrbracket\sigma = \llbracket a_2[a/x] \rrbracket\sigma [ [ a 1 [ a / x ] ] ] σ = [ [ a 2 [ a / x ] ] ] σ と同値で、いま示した第 1 式によりこれは ⟦ a 1 ⟧ τ = ⟦ a 2 ⟧ τ \llbracket a_1 \rrbracket\tau = \llbracket a_2 \rrbracket\tau [ [ a 1 ] ] τ = [ [ a 2 ] ] τ 、すなわち ⟦ b ⟧ τ = t t \llbracket b \rrbracket\tau = \mathrm{tt} [ [ b ] ] τ = tt と同値です。≤ \le ≤ も同じです。¬ b 1 \lnot b_1 ¬ b 1 と b 1 ∧ b 2 b_1 \wedge b_2 b 1 ∧ b 2 は、値が部分式の値の関数なので帰納法の仮定から従います。
最後の主張は、{ σ ∣ ⟦ b [ a / x ] ⟧ σ = t t } = { σ ∣ ⟦ b ⟧ ( σ [ x ↦ ⟦ a ⟧ σ ] ) = t t } \{\sigma \mid \llbracket b[a/x] \rrbracket\sigma = \mathrm{tt}\} = \{\sigma \mid \llbracket b \rrbracket(\sigma[x \mapsto \llbracket a \rrbracket\sigma]) = \mathrm{tt}\} { σ ∣ [ [ b [ a / x ] ] ] σ = tt } = { σ ∣ [ [ b ] ] ( σ [ x ↦ [ [ a ] ] σ ]) = tt } であり、右辺は 定義 3.4 の定義そのものです。
∎
命題 3.6 (代入の規則は後ろ向きである ) ✓
x x x を変数、a a a を算術式、P , Q P, Q P , Q を表明とする。解釈は任意でよい。
⊨ { Q [ a / x ] } x : = a { Q } \models \{Q[a/x]\}\ x := a\ \{Q\} ⊨ { Q [ a / x ]} x := a { Q } が成り立つ。
⊨ { P } x : = a { Q } \models \{P\}\ x := a\ \{Q\} ⊨ { P } x := a { Q } ならば P ⇒ Q [ a / x ] P \Rightarrow Q[a/x] P ⇒ Q [ a / x ] である。すなわち Q [ a / x ] Q[a/x] Q [ a / x ] は、事後条件 Q Q Q を保証する事前条件のうちでもっとも弱い。
「前向き」に書いた規則 { P } x : = a { P ∧ x = a } \{P\}\ x := a\ \{P \wedge x = a\} { P } x := a { P ∧ x = a } は正しくない。P P P を x = 0 x = 0 x = 0 、a a a を x + 1 x + 1 x + 1 とすると、三つ組 { x = 0 } x : = x + 1 { x = 0 ∧ x = x + 1 } \{x = 0\}\ x := x + 1\ \{x = 0 \wedge x = x + 1\} { x = 0 } x := x + 1 { x = 0 ∧ x = x + 1 } は部分正当でない。
証明(命題 3.6) (1) Q [ a / x ] ( σ ) Q[a/x](\sigma) Q [ a / x ] ( σ ) かつ ⟨ x : = a , σ ⟩ ⇓ σ ′ \langle x := a, \sigma \rangle \Downarrow \sigma' ⟨ x := a , σ ⟩ ⇓ σ ′ とします。代入を結論にもつ規則は (Assign) だけなので σ ′ = σ [ x ↦ ⟦ a ⟧ σ ] \sigma' = \sigma[x \mapsto \llbracket a \rrbracket\sigma] σ ′ = σ [ x ↦ [ [ a ] ] σ ] です。定義 3.4 より、Q [ a / x ] ( σ ) Q[a/x](\sigma) Q [ a / x ] ( σ ) は Q ( σ [ x ↦ ⟦ a ⟧ σ ] ) Q(\sigma[x \mapsto \llbracket a \rrbracket\sigma]) Q ( σ [ x ↦ [ [ a ] ] σ ]) 、つまり Q ( σ ′ ) Q(\sigma') Q ( σ ′ ) のことです。
(2) P ( σ ) P(\sigma) P ( σ ) とします。(Assign) により σ ′ = σ [ x ↦ ⟦ a ⟧ σ ] \sigma' = \sigma[x \mapsto \llbracket a \rrbracket\sigma] σ ′ = σ [ x ↦ [ [ a ] ] σ ] について ⟨ x : = a , σ ⟩ ⇓ σ ′ \langle x := a, \sigma \rangle \Downarrow \sigma' ⟨ x := a , σ ⟩ ⇓ σ ′ が成り立つので、仮定 ⊨ { P } x : = a { Q } \models \{P\}\ x := a\ \{Q\} ⊨ { P } x := a { Q } から Q ( σ ′ ) Q(\sigma') Q ( σ ′ ) です。これは Q [ a / x ] ( σ ) Q[a/x](\sigma) Q [ a / x ] ( σ ) の定義です。
(3) σ ( x ) = 0 \sigma(x) = 0 σ ( x ) = 0 である状態 σ \sigma σ をとります。P ( σ ) P(\sigma) P ( σ ) は成り立ち、⟨ x : = x + 1 , σ ⟩ ⇓ σ ′ \langle x := x + 1, \sigma \rangle \Downarrow \sigma' ⟨ x := x + 1 , σ ⟩ ⇓ σ ′ 、σ ′ = σ [ x ↦ 1 ] \sigma' = \sigma[x \mapsto 1] σ ′ = σ [ x ↦ 1 ] です。事後条件 x = 0 ∧ x = x + 1 x = 0 \wedge x = x + 1 x = 0 ∧ x = x + 1 は、σ ′ ( x ) = σ ′ ( x ) + 1 \sigma'(x) = \sigma'(x) + 1 σ ′ ( x ) = σ ′ ( x ) + 1 を満たす状態が無いので空集合(f a l s e \mathrm{false} false )です。σ ′ \sigma' σ ′ はこれに属しません。
∎
(3) の規則が壊れるのは、事後条件の中の x x x が代入後 の値を指すのに、P P P と a a a の中の x x x は代入前 の値を指しているからです。x x x が P P P にも a a a にも現れなければ、前向きの規則は正しくなります。後ろ向きの規則は、(1) の証明が 1 行で済んだとおり、意味論の (Assign) をそのまま写しています。事後条件 Q Q Q の中の x x x を「これから入る値」a a a に置き換えれば、それが代入前に成り立つべき条件です。たとえば { x + 1 ≤ 10 } x : = x + 1 { x ≤ 10 } \{x + 1 \le 10\}\ x := x + 1\ \{x \le 10\} { x + 1 ≤ 10 } x := x + 1 { x ≤ 10 } で、事前条件は x ≤ 9 x \le 9 x ≤ 9 です。
定義 3.7 (Hoare 論理の推論規則 ) ↗
契約表 C \mathcal{C} C とは、操作名 m ∈ O p m \in \mathrm{Op} m ∈ Op と表明 P , Q P, Q P , Q の三つ組 ( m , P , Q ) (m, P, Q) ( m , P , Q ) の集合をいう。解釈 M \mathcal{M} M が契約表 C \mathcal{C} C を満たす とは、すべての ( m , P , Q ) ∈ C (m, P, Q) \in \mathcal{C} ( m , P , Q ) ∈ C について ⊨ M { P } c a l l m { Q } \models_{\mathcal{M}} \{P\}\ \mathtt{call}\ m\ \{Q\} ⊨ M { P } call m { Q } が成り立つことをいう。
契約表 C \mathcal{C} C のもとでの導出可能性 ⊢ C { P } c { Q } \vdash_{\mathcal{C}} \{P\}\ c\ \{Q\} ⊢ C { P } c { Q } を、次の規則で生成される最小の関係とする。
(H-Skip) { P } s k i p { P } (H-Assign) { Q [ a / x ] } x : = a { Q } (H-Seq) { P } c 1 { R } { R } c 2 { Q } { P } c 1 ; c 2 { Q } (H-Call) ( m , P , Q ) ∈ C { P } c a l l m { Q } (H-If) { P ∧ b } c 1 { Q } { P ∧ ¬ b } c 2 { Q } { P } i f b t h e n c 1 e l s e c 2 { Q } (H-While) { I ∧ b } c { I } { I } w h i l e b d o c { I ∧ ¬ b } (H-Conseq) P ⇒ P ′ { P ′ } c { Q ′ } Q ′ ⇒ Q { P } c { Q } \begin{aligned}
&\text{(H-Skip)} && \{P\}\ \mathtt{skip}\ \{P\}
\qquad\qquad \text{(H-Assign)} \quad \{Q[a/x]\}\ x := a\ \{Q\} \\[4pt]
&\text{(H-Seq)} && \frac{\{P\}\ c_1\ \{R\} \qquad \{R\}\ c_2\ \{Q\}}{\{P\}\ c_1 ; c_2\ \{Q\}}
\qquad\qquad \text{(H-Call)} \quad \frac{(m, P, Q) \in \mathcal{C}}{\{P\}\ \mathtt{call}\ m\ \{Q\}} \\[4pt]
&\text{(H-If)} && \frac{\{P \wedge b\}\ c_1\ \{Q\} \qquad \{P \wedge \lnot b\}\ c_2\ \{Q\}}{\{P\}\ \mathtt{if}\ b\ \mathtt{then}\ c_1\ \mathtt{else}\ c_2\ \{Q\}} \\[4pt]
&\text{(H-While)} && \frac{\{I \wedge b\}\ c\ \{I\}}{\{I\}\ \mathtt{while}\ b\ \mathtt{do}\ c\ \{I \wedge \lnot b\}} \\[4pt]
&\text{(H-Conseq)} && \frac{P \Rightarrow P' \qquad \{P'\}\ c\ \{Q'\} \qquad Q' \Rightarrow Q}{\{P\}\ c\ \{Q\}}
\end{aligned} (H-Skip) (H-Seq) (H-If) (H-While) (H-Conseq) { P } skip { P } (H-Assign) { Q [ a / x ]} x := a { Q } { P } c 1 ; c 2 { Q } { P } c 1 { R } { R } c 2 { Q } (H-Call) { P } call m { Q } ( m , P , Q ) ∈ C { P } if b then c 1 else c 2 { Q } { P ∧ b } c 1 { Q } { P ∧ ¬ b } c 2 { Q } { I } while b do c { I ∧ ¬ b } { I ∧ b } c { I } { P } c { Q } P ⇒ P ′ { P ′ } c { Q ′ } Q ′ ⇒ Q (H-While) の表明 I I I をループ不変条件 、(H-Conseq) を帰結の規則 という。
規則は命令の構文 1 つにつき 1 つあり、(H-Conseq) だけが構文によらない規則です。(H-Conseq) は「事前条件は強めてよく、事後条件は弱めてよい」と読めます。この規則が帰結の規則と呼ばれるのは、すでに導いた三つ組から、条件を弱めたり強めたりした別の三つ組を結論できるからです。(H-While) は、ループ本体が「I I I と条件 b b b が成り立つ状態から、I I I が成り立つ状態へ」移ることだけを要求し、反復の回数には触れません。ループを抜けたときに言えるのは、I I I と、抜けた理由である ¬ b \lnot b ¬ b です。
定理 3.8 (Hoare 論理の健全性 ) ✓
C \mathcal{C} C を契約表、M \mathcal{M} M を C \mathcal{C} C を満たす解釈とする。⊢ C { P } c { Q } \vdash_{\mathcal{C}} \{P\}\ c\ \{Q\} ⊢ C { P } c { Q } ならば ⊨ M { P } c { Q } \models_{\mathcal{M}} \{P\}\ c\ \{Q\} ⊨ M { P } c { Q } である。
証明の方針を先に述べます。規則の数だけ場合分けをし、ほとんどは意味論の規則をそのまま写すだけで済みます。手間がかかるのは while だけで、ここでは帰納法を二重に使います。論理の導出についての帰納法が本体の事実を 1 つ与え、実行の導出についての帰納法が、それを反復の回数だけ繰り返し使います。
証明(定理 3.8) ⊢ C { P } c { Q } \vdash_{\mathcal{C}} \{P\}\ c\ \{Q\} ⊢ C { P } c { Q } の導出の高さについての完全帰納法(定理 4.3[証明の技術] )で示します。導出の最後の規則で場合を分けます。以下、⊨ \models ⊨ は ⊨ M \models_{\mathcal{M}} ⊨ M の意味です。
(H-Skip)。P ( σ ) P(\sigma) P ( σ ) かつ ⟨ s k i p , σ ⟩ ⇓ σ ′ \langle \mathtt{skip}, \sigma \rangle \Downarrow \sigma' ⟨ skip , σ ⟩ ⇓ σ ′ とします。適用できる規則は (Skip) だけなので σ ′ = σ \sigma' = \sigma σ ′ = σ 、よって P ( σ ′ ) P(\sigma') P ( σ ′ ) です。
(H-Assign)。命題 3.6 (1) です。
(H-Call)。( m , P , Q ) ∈ C (m, P, Q) \in \mathcal{C} ( m , P , Q ) ∈ C で、M \mathcal{M} M は C \mathcal{C} C を満たすので、定義から ⊨ { P } c a l l m { Q } \models \{P\}\ \mathtt{call}\ m\ \{Q\} ⊨ { P } call m { Q } です。
(H-Seq)。帰納法の仮定から ⊨ { P } c 1 { R } \models \{P\}\ c_1\ \{R\} ⊨ { P } c 1 { R } と ⊨ { R } c 2 { Q } \models \{R\}\ c_2\ \{Q\} ⊨ { R } c 2 { Q } です。P ( σ ) P(\sigma) P ( σ ) かつ ⟨ c 1 ; c 2 , σ ⟩ ⇓ σ ′ \langle c_1; c_2, \sigma \rangle \Downarrow \sigma' ⟨ c 1 ; c 2 , σ ⟩ ⇓ σ ′ とします。この実行の導出は (Seq) で終わるので、⟨ c 1 , σ ⟩ ⇓ σ 1 \langle c_1, \sigma \rangle \Downarrow \sigma_1 ⟨ c 1 , σ ⟩ ⇓ σ 1 かつ ⟨ c 2 , σ 1 ⟩ ⇓ σ ′ \langle c_2, \sigma_1 \rangle \Downarrow \sigma' ⟨ c 2 , σ 1 ⟩ ⇓ σ ′ となる σ 1 \sigma_1 σ 1 があります。1 つ目の仮定から R ( σ 1 ) R(\sigma_1) R ( σ 1 ) 、2 つ目の仮定から Q ( σ ′ ) Q(\sigma') Q ( σ ′ ) です。
(H-If)。帰納法の仮定から ⊨ { P ∧ b } c 1 { Q } \models \{P \wedge b\}\ c_1\ \{Q\} ⊨ { P ∧ b } c 1 { Q } と ⊨ { P ∧ ¬ b } c 2 { Q } \models \{P \wedge \lnot b\}\ c_2\ \{Q\} ⊨ { P ∧ ¬ b } c 2 { Q } です。P ( σ ) P(\sigma) P ( σ ) かつ ⟨ i f b t h e n c 1 e l s e c 2 , σ ⟩ ⇓ σ ′ \langle \mathtt{if}\ b\ \mathtt{then}\ c_1\ \mathtt{else}\ c_2, \sigma \rangle \Downarrow \sigma' ⟨ if b then c 1 else c 2 , σ ⟩ ⇓ σ ′ とします。実行の導出が (IfT) で終わるなら、⟦ b ⟧ σ = t t \llbracket b \rrbracket\sigma = \mathrm{tt} [ [ b ] ] σ = tt かつ ⟨ c 1 , σ ⟩ ⇓ σ ′ \langle c_1, \sigma \rangle \Downarrow \sigma' ⟨ c 1 , σ ⟩ ⇓ σ ′ で、( P ∧ b ) ( σ ) (P \wedge b)(\sigma) ( P ∧ b ) ( σ ) なので 1 つ目の仮定から Q ( σ ′ ) Q(\sigma') Q ( σ ′ ) です。(IfF) で終わるなら、⟦ b ⟧ σ = f f \llbracket b \rrbracket\sigma = \mathrm{ff} [ [ b ] ] σ = ff かつ ⟨ c 2 , σ ⟩ ⇓ σ ′ \langle c_2, \sigma \rangle \Downarrow \sigma' ⟨ c 2 , σ ⟩ ⇓ σ ′ で、( P ∧ ¬ b ) ( σ ) (P \wedge \lnot b)(\sigma) ( P ∧ ¬ b ) ( σ ) なので 2 つ目の仮定から Q ( σ ′ ) Q(\sigma') Q ( σ ′ ) です。
(H-Conseq)。帰納法の仮定から ⊨ { P ′ } c { Q ′ } \models \{P'\}\ c\ \{Q'\} ⊨ { P ′ } c { Q ′ } です。P ( σ ) P(\sigma) P ( σ ) かつ ⟨ c , σ ⟩ ⇓ σ ′ \langle c, \sigma \rangle \Downarrow \sigma' ⟨ c , σ ⟩ ⇓ σ ′ とします。P ⇒ P ′ P \Rightarrow P' P ⇒ P ′ より P ′ ( σ ) P'(\sigma) P ′ ( σ ) 、仮定より Q ′ ( σ ′ ) Q'(\sigma') Q ′ ( σ ′ ) 、Q ′ ⇒ Q Q' \Rightarrow Q Q ′ ⇒ Q より Q ( σ ′ ) Q(\sigma') Q ( σ ′ ) です。
(H-While)。w = w h i l e b d o c w = \mathtt{while}\ b\ \mathtt{do}\ c w = while b do c とおきます。外側の帰納法の仮定から ⊨ { I ∧ b } c { I } \models \{I \wedge b\}\ c\ \{I\} ⊨ { I ∧ b } c { I } です。示すことは、すべての σ , σ ′ \sigma, \sigma' σ , σ ′ について
I ( σ ) I(\sigma) I ( σ ) かつ ⟨ w , σ ⟩ ⇓ σ ′ \langle w, \sigma \rangle \Downarrow \sigma' ⟨ w , σ ⟩ ⇓ σ ′ ならば、I ( σ ′ ) I(\sigma') I ( σ ′ ) かつ ⟦ b ⟧ σ ′ = f f \llbracket b \rrbracket\sigma' = \mathrm{ff} [ [ b ] ] σ ′ = ff
です。これを、実行 ⟨ w , σ ⟩ ⇓ σ ′ \langle w, \sigma \rangle \Downarrow \sigma' ⟨ w , σ ⟩ ⇓ σ ′ の導出の高さについての完全帰納法(内側の帰納法)で示します。w w w を結論にもつ規則は (WhF) と (WhT) だけです。
実行の導出が (WhF) で終わるとき、⟦ b ⟧ σ = f f \llbracket b \rrbracket\sigma = \mathrm{ff} [ [ b ] ] σ = ff かつ σ ′ = σ \sigma' = \sigma σ ′ = σ です。I ( σ ) I(\sigma) I ( σ ) は仮定なので、I ( σ ′ ) I(\sigma') I ( σ ′ ) かつ ⟦ b ⟧ σ ′ = f f \llbracket b \rrbracket\sigma' = \mathrm{ff} [ [ b ] ] σ ′ = ff です。
実行の導出が (WhT) で終わるとき、⟦ b ⟧ σ = t t \llbracket b \rrbracket\sigma = \mathrm{tt} [ [ b ] ] σ = tt 、⟨ c , σ ⟩ ⇓ σ 1 \langle c, \sigma \rangle \Downarrow \sigma_1 ⟨ c , σ ⟩ ⇓ σ 1 、⟨ w , σ 1 ⟩ ⇓ σ ′ \langle w, \sigma_1 \rangle \Downarrow \sigma' ⟨ w , σ 1 ⟩ ⇓ σ ′ となる σ 1 \sigma_1 σ 1 があります。( I ∧ b ) ( σ ) (I \wedge b)(\sigma) ( I ∧ b ) ( σ ) なので、⊨ { I ∧ b } c { I } \models \{I \wedge b\}\ c\ \{I\} ⊨ { I ∧ b } c { I } から I ( σ 1 ) I(\sigma_1) I ( σ 1 ) です。⟨ w , σ 1 ⟩ ⇓ σ ′ \langle w, \sigma_1 \rangle \Downarrow \sigma' ⟨ w , σ 1 ⟩ ⇓ σ ′ の導出は元の実行の導出の部分木で、高さが真に小さいので、内側の帰納法の仮定を σ 1 \sigma_1 σ 1 に使えて、I ( σ ′ ) I(\sigma') I ( σ ′ ) かつ ⟦ b ⟧ σ ′ = f f \llbracket b \rrbracket\sigma' = \mathrm{ff} [ [ b ] ] σ ′ = ff です。
以上で ⊨ { I } w { I ∧ ¬ b } \models \{I\}\ w\ \{I \wedge \lnot b\} ⊨ { I } w { I ∧ ¬ b } が示されました。
∎
while の場合に帰納法が二重になるのは、Hoare 論理の導出の中では while は 1 回しか現れないのに、実行の導出の中では反復の回数だけ (WhT) が現れるからです。外側の帰納法(論理の導出)は本体についての事実を 1 つ与え、内側の帰納法(実行の導出)がそれを反復の回数だけ繰り返し使います。ループ不変条件は、この内側の帰納法の「帰納法の仮定」を表明として書いたものです。幅優先探索の正しさを 3 つの不変式で示した証明(補題 3.1[グラフアルゴリズム] )も、同じ形をしています。
例 3.10 (二分探索の検証条件 )
探索アルゴリズム の lower_bound を言語 W で書きます。配列は書き換えないので、状態には入れず、固定した関数 A : Z → Z A : \mathbb{Z} \to \mathbb{Z} A : Z → Z (添字 0 , … , n − 1 0, \ldots, n - 1 0 , … , n − 1 の外では 0 0 0 )として、式 A [ a ] A[a] A [ a ] の値を A ( ⟦ a ⟧ σ ) A(\llbracket a \rrbracket\sigma) A ( [ [ a ] ] σ ) と定めます。これは状態の全域関数なので、ここまでの証明はそのまま通ります。n ≥ 0 n \ge 0 n ≥ 0 と x x x も定数とします。
l o : = 0 ; h i : = n ; w h i l e l o < h i d o ( m i d : = l o + ( h i − l o ) ÷ 2 ; i f A [ m i d ] < x t h e n l o : = m i d + 1 e l s e h i : = m i d ) \begin{aligned}
&lo := 0;\ hi := n; \\
&\mathtt{while}\ lo \lt hi\ \mathtt{do}\ \bigl(\, mid := lo + (hi - lo) \div 2;\ \mathtt{if}\ A[mid] \lt x\ \mathtt{then}\ lo := mid + 1\ \mathtt{else}\ hi := mid \,\bigr)
\end{aligned} l o := 0 ; hi := n ; while l o < hi do ( mi d := l o + ( hi − l o ) ÷ 2 ; if A [ mi d ] < x then l o := mi d + 1 else hi := mi d ) 事前条件は「A A A は 0 , … , n − 1 0, \ldots, n - 1 0 , … , n − 1 で昇順」ですが、A A A は定数なので、これは状態によらない仮定です。ループ不変条件 I I I は、既存の証明(定理 3.2[探索アルゴリズム] )の 3 条件の連言です。
I : 0 ≤ l o ≤ h i ≤ n ∧ ( ∀ i < l o , A [ i ] < x ) ∧ ( ∀ i ≥ h i , i < n ⇒ A [ i ] ≥ x ) I:\quad 0 \le lo \le hi \le n \ \wedge\ \bigl(\forall i \lt lo,\ A[i] \lt x\bigr) \ \wedge\ \bigl(\forall i \ge hi,\ i \lt n \Rightarrow A[i] \ge x\bigr) I : 0 ≤ l o ≤ hi ≤ n ∧ ( ∀ i < l o , A [ i ] < x ) ∧ ( ∀ i ≥ hi , i < n ⇒ A [ i ] ≥ x ) (i i i は 0 ≤ i < n 0 \le i \lt n 0 ≤ i < n を動きます。)Hoare 論理の規則を後ろからたどると、示すべきことは次の 3 つの含意(検証条件 )に帰着します。
(V1) 初期化。 { t r u e } l o : = 0 ; h i : = n { I } \{\mathrm{true}\}\ lo := 0;\ hi := n\ \{I\} { true } l o := 0 ; hi := n { I } を導くには、(H-Assign) を 2 回使って事前条件 ( I [ n / h i ] ) [ 0 / l o ] (I[n/hi])[0/lo] ( I [ n / hi ]) [ 0/ l o ] を得ます。これは 0 ≤ 0 ≤ n ≤ n 0 \le 0 \le n \le n 0 ≤ 0 ≤ n ≤ n と「i < 0 i \lt 0 i < 0 なら…」「i ≥ n i \ge n i ≥ n かつ i < n i \lt n i < n なら…」の連言で、n ≥ 0 n \ge 0 n ≥ 0 のもとで成り立ちます。(H-Conseq) で t r u e \mathrm{true} true から導けます。
(V2) 保存。 本体を c c c とします。{ I ∧ l o < h i } c { I } \{I \wedge lo \lt hi\}\ c\ \{I\} { I ∧ l o < hi } c { I } を導くには、(H-If) と (H-Assign) から、m i d mid mi d への代入のあとで
( A [ m i d ] < x ⇒ I [ m i d + 1 / l o ] ) ∧ ( A [ m i d ] ≥ x ⇒ I [ m i d / h i ] ) \bigl(A[mid] \lt x \Rightarrow I[mid + 1/lo]\bigr) \ \wedge\ \bigl(A[mid] \ge x \Rightarrow I[mid/hi]\bigr) ( A [ mi d ] < x ⇒ I [ mi d + 1/ l o ] ) ∧ ( A [ mi d ] ≥ x ⇒ I [ mi d / hi ] ) が成り立てば十分で、さらに m i d mid mi d に l o + ( h i − l o ) ÷ 2 lo + (hi - lo) \div 2 l o + ( hi − l o ) ÷ 2 を代入したものが I ∧ l o < h i I \wedge lo \lt hi I ∧ l o < hi から従えば十分です。中身は「l o ≤ m i d < h i lo \le mid \lt hi l o ≤ mi d < hi 」「A [ m i d ] < x A[mid] \lt x A [ mi d ] < x なら i ≤ m i d i \le mid i ≤ mi d のすべてで A [ i ] < x A[i] \lt x A [ i ] < x 」「A [ m i d ] ≥ x A[mid] \ge x A [ mi d ] ≥ x なら i ≥ m i d i \ge mid i ≥ mi d のすべてで A [ i ] ≥ x A[i] \ge x A [ i ] ≥ x 」で、既存の証明の「中央位置の範囲」と「保存」の段がちょうどこれを示しています。
(V3) 終了時。 (H-While) の結論の事後条件は I ∧ ¬ ( l o < h i ) I \wedge \lnot(lo \lt hi) I ∧ ¬ ( l o < hi ) です。I I I の l o ≤ h i lo \le hi l o ≤ hi と合わせて l o = h i lo = hi l o = hi で、I I I の残りから l o lo l o は A [ i ] < x A[i] \lt x A [ i ] < x となる i i i の個数、すなわち下限位置 l b A ( x ) \mathrm{lb}_A(x) lb A ( x ) (定義 3.1[探索アルゴリズム] )に等しいことが従います。(H-Conseq) で事後条件 l o = l b A ( x ) lo = \mathrm{lb}_A(x) l o = lb A ( x ) を得ます。
(V1)〜(V3) から 定理 3.8 により ⊨ { t r u e } (上のプログラム) { l o = l b A ( x ) } \models \{\mathrm{true}\}\ \text{(上のプログラム)}\ \{lo = \mathrm{lb}_A(x)\} ⊨ { true } (上のプログラム) { l o = lb A ( x )} です。Hoare 論理は、既存の証明の「初期化・保存・終了時」という段取りを、規則の適用として固定したものだと分かります。人が考える必要があるのは、不変条件 I I I を見つけることと、3 つの含意を示すことだけです。
命題 3.6 (2) は、代入について「事後条件 Q Q Q を保証するもっとも弱い事前条件」が Q [ a / x ] Q[a/x] Q [ a / x ] であることを述べていました。E. W. Dijkstra は、これをすべての命令に広げ、プログラムの意味を「事後条件から事前条件への写像」として捉えました。
定義 4.1 (最弱事前条件 ) ↗
解釈 M \mathcal{M} M 、命令 c c c 、表明 Q Q Q に対して
w l p ( c , Q ) = { σ ∣ すべての σ ′ について、 ⟨ c , σ ⟩ ⇓ σ ′ ならば Q ( σ ′ ) } , w p ( c , Q ) = { σ ∣ c は σ から停止し、すべての σ ′ について、 ⟨ c , σ ⟩ ⇓ σ ′ ならば Q ( σ ′ ) } \begin{aligned}
\mathrm{wlp}(c, Q) &= \{\, \sigma \mid \text{すべての } \sigma' \text{ について、} \langle c, \sigma \rangle \Downarrow \sigma' \text{ ならば } Q(\sigma') \,\}, \\
\mathrm{wp}(c, Q) &= \{\, \sigma \mid c \text{ は } \sigma \text{ から停止し、すべての } \sigma' \text{ について、} \langle c, \sigma \rangle \Downarrow \sigma' \text{ ならば } Q(\sigma') \,\}
\end{aligned} wlp ( c , Q ) wp ( c , Q ) = { σ ∣ すべての σ ′ について、 ⟨ c , σ ⟩ ⇓ σ ′ ならば Q ( σ ′ ) } , = { σ ∣ c は σ から停止し、すべての σ ′ について、 ⟨ c , σ ⟩ ⇓ σ ′ ならば Q ( σ ′ ) } と定める。w l p ( c , Q ) \mathrm{wlp}(c, Q) wlp ( c , Q ) を最弱自由事前条件 (weakest liberal precondition)、w p ( c , Q ) \mathrm{wp}(c, Q) wp ( c , Q ) を最弱事前条件 (weakest precondition)という。
定理 4.2 (最弱事前条件の性質 ) ✓
M \mathcal{M} M を解釈、c , c 1 , c 2 c, c_1, c_2 c , c 1 , c 2 を命令、P , Q , Q 1 , Q 2 P, Q, Q_1, Q_2 P , Q , Q 1 , Q 2 を表明とする。
(特徴づけ)⊨ M { P } c { Q } \models_{\mathcal{M}} \{P\}\ c\ \{Q\} ⊨ M { P } c { Q } は P ⇒ w l p ( c , Q ) P \Rightarrow \mathrm{wlp}(c, Q) P ⇒ wlp ( c , Q ) と同値である。また P ⇒ w p ( c , Q ) P \Rightarrow \mathrm{wp}(c, Q) P ⇒ wp ( c , Q ) ならば ⊨ M [ P ] c [ Q ] \models_{\mathcal{M}} [P]\ c\ [Q] ⊨ M [ P ] c [ Q ] である。
(単調性)Q 1 ⇒ Q 2 Q_1 \Rightarrow Q_2 Q 1 ⇒ Q 2 ならば w l p ( c , Q 1 ) ⇒ w l p ( c , Q 2 ) \mathrm{wlp}(c, Q_1) \Rightarrow \mathrm{wlp}(c, Q_2) wlp ( c , Q 1 ) ⇒ wlp ( c , Q 2 ) かつ w p ( c , Q 1 ) ⇒ w p ( c , Q 2 ) \mathrm{wp}(c, Q_1) \Rightarrow \mathrm{wp}(c, Q_2) wp ( c , Q 1 ) ⇒ wp ( c , Q 2 ) 。
(連言)w l p ( c , Q 1 ∧ Q 2 ) = w l p ( c , Q 1 ) ∧ w l p ( c , Q 2 ) \mathrm{wlp}(c, Q_1 \wedge Q_2) = \mathrm{wlp}(c, Q_1) \wedge \mathrm{wlp}(c, Q_2) wlp ( c , Q 1 ∧ Q 2 ) = wlp ( c , Q 1 ) ∧ wlp ( c , Q 2 ) 、w p ( c , Q 1 ∧ Q 2 ) = w p ( c , Q 1 ) ∧ w p ( c , Q 2 ) \mathrm{wp}(c, Q_1 \wedge Q_2) = \mathrm{wp}(c, Q_1) \wedge \mathrm{wp}(c, Q_2) wp ( c , Q 1 ∧ Q 2 ) = wp ( c , Q 1 ) ∧ wp ( c , Q 2 ) 。
(選言)M \mathcal{M} M がすべての操作名について部分関数ならば、w p ( c , Q 1 ∨ Q 2 ) = w p ( c , Q 1 ) ∨ w p ( c , Q 2 ) \mathrm{wp}(c, Q_1 \vee Q_2) = \mathrm{wp}(c, Q_1) \vee \mathrm{wp}(c, Q_2) wp ( c , Q 1 ∨ Q 2 ) = wp ( c , Q 1 ) ∨ wp ( c , Q 2 ) 、w l p ( c , Q 1 ∨ Q 2 ) = w l p ( c , Q 1 ) ∨ w l p ( c , Q 2 ) \mathrm{wlp}(c, Q_1 \vee Q_2) = \mathrm{wlp}(c, Q_1) \vee \mathrm{wlp}(c, Q_2) wlp ( c , Q 1 ∨ Q 2 ) = wlp ( c , Q 1 ) ∨ wlp ( c , Q 2 ) 。さらにこのとき、⊨ M [ P ] c [ Q ] \models_{\mathcal{M}} [P]\ c\ [Q] ⊨ M [ P ] c [ Q ] は P ⇒ w p ( c , Q ) P \Rightarrow \mathrm{wp}(c, Q) P ⇒ wp ( c , Q ) と同値である。
(計算規則)w l p ( s k i p , Q ) = Q \mathrm{wlp}(\mathtt{skip}, Q) = Q wlp ( skip , Q ) = Q 、w l p ( x : = a , Q ) = Q [ a / x ] \mathrm{wlp}(x := a, Q) = Q[a/x] wlp ( x := a , Q ) = Q [ a / x ] 、w l p ( c 1 ; c 2 , Q ) = w l p ( c 1 , w l p ( c 2 , Q ) ) \mathrm{wlp}(c_1; c_2, Q) = \mathrm{wlp}(c_1, \mathrm{wlp}(c_2, Q)) wlp ( c 1 ; c 2 , Q ) = wlp ( c 1 , wlp ( c 2 , Q )) 、
w l p ( i f b t h e n c 1 e l s e c 2 , Q ) = ( b ∧ w l p ( c 1 , Q ) ) ∨ ( ¬ b ∧ w l p ( c 2 , Q ) ) \mathrm{wlp}(\mathtt{if}\ b\ \mathtt{then}\ c_1\ \mathtt{else}\ c_2, Q) = \bigl(b \wedge \mathrm{wlp}(c_1, Q)\bigr) \vee \bigl(\lnot b \wedge \mathrm{wlp}(c_2, Q)\bigr) wlp ( if b then c 1 else c 2 , Q ) = ( b ∧ wlp ( c 1 , Q ) ) ∨ ( ¬ b ∧ wlp ( c 2 , Q ) ) 。
証明(定理 4.2) (1) ⊨ { P } c { Q } \models \{P\}\ c\ \{Q\} ⊨ { P } c { Q } は「すべての σ \sigma σ について、P ( σ ) P(\sigma) P ( σ ) ならば(すべての σ ′ \sigma' σ ′ について、⟨ c , σ ⟩ ⇓ σ ′ \langle c, \sigma \rangle \Downarrow \sigma' ⟨ c , σ ⟩ ⇓ σ ′ ならば Q ( σ ′ ) Q(\sigma') Q ( σ ′ ) )」と書き直せます。括弧の中は σ ∈ w l p ( c , Q ) \sigma \in \mathrm{wlp}(c, Q) σ ∈ wlp ( c , Q ) の定義なので、これは P ⊆ w l p ( c , Q ) P \subseteq \mathrm{wlp}(c, Q) P ⊆ wlp ( c , Q ) です。後半。P ⇒ w p ( c , Q ) P \Rightarrow \mathrm{wp}(c, Q) P ⇒ wp ( c , Q ) とし、P ( σ ) P(\sigma) P ( σ ) とします。σ ∈ w p ( c , Q ) \sigma \in \mathrm{wp}(c, Q) σ ∈ wp ( c , Q ) なので c c c は σ \sigma σ から停止し、⟨ c , σ ⟩ ⇓ σ ′ \langle c, \sigma \rangle \Downarrow \sigma' ⟨ c , σ ⟩ ⇓ σ ′ となる σ ′ \sigma' σ ′ があり、定義の後半から Q ( σ ′ ) Q(\sigma') Q ( σ ′ ) です。これは全正当性の定義です。
(2) σ ∈ w l p ( c , Q 1 ) \sigma \in \mathrm{wlp}(c, Q_1) σ ∈ wlp ( c , Q 1 ) とし、⟨ c , σ ⟩ ⇓ σ ′ \langle c, \sigma \rangle \Downarrow \sigma' ⟨ c , σ ⟩ ⇓ σ ′ とします。Q 1 ( σ ′ ) Q_1(\sigma') Q 1 ( σ ′ ) で、Q 1 ⇒ Q 2 Q_1 \Rightarrow Q_2 Q 1 ⇒ Q 2 より Q 2 ( σ ′ ) Q_2(\sigma') Q 2 ( σ ′ ) です。よって σ ∈ w l p ( c , Q 2 ) \sigma \in \mathrm{wlp}(c, Q_2) σ ∈ wlp ( c , Q 2 ) 。w p \mathrm{wp} wp は、これに「σ \sigma σ から停止する」という Q Q Q によらない条件が付くだけなので同じです。
(3) σ ∈ w l p ( c , Q 1 ∧ Q 2 ) \sigma \in \mathrm{wlp}(c, Q_1 \wedge Q_2) σ ∈ wlp ( c , Q 1 ∧ Q 2 ) は「すべての σ ′ \sigma' σ ′ について、⟨ c , σ ⟩ ⇓ σ ′ \langle c, \sigma \rangle \Downarrow \sigma' ⟨ c , σ ⟩ ⇓ σ ′ ならば Q 1 ( σ ′ ) Q_1(\sigma') Q 1 ( σ ′ ) かつ Q 2 ( σ ′ ) Q_2(\sigma') Q 2 ( σ ′ ) 」です。「すべての σ ′ \sigma' σ ′ について A ( σ ′ ) A(\sigma') A ( σ ′ ) ならば(B 1 ( σ ′ ) B_1(\sigma') B 1 ( σ ′ ) かつ B 2 ( σ ′ ) B_2(\sigma') B 2 ( σ ′ ) )」は「(すべての σ ′ \sigma' σ ′ について A ⇒ B 1 A \Rightarrow B_1 A ⇒ B 1 )かつ(すべての σ ′ \sigma' σ ′ について A ⇒ B 2 A \Rightarrow B_2 A ⇒ B 2 )」と同値なので、これは σ ∈ w l p ( c , Q 1 ) \sigma \in \mathrm{wlp}(c, Q_1) σ ∈ wlp ( c , Q 1 ) かつ σ ∈ w l p ( c , Q 2 ) \sigma \in \mathrm{wlp}(c, Q_2) σ ∈ wlp ( c , Q 2 ) です。w p \mathrm{wp} wp では両辺に同じ条件「σ \sigma σ から停止する」が付きます。
(4) Q i ⇒ Q 1 ∨ Q 2 Q_i \Rightarrow Q_1 \vee Q_2 Q i ⇒ Q 1 ∨ Q 2 なので、(2) より w p ( c , Q i ) ⇒ w p ( c , Q 1 ∨ Q 2 ) \mathrm{wp}(c, Q_i) \Rightarrow \mathrm{wp}(c, Q_1 \vee Q_2) wp ( c , Q i ) ⇒ wp ( c , Q 1 ∨ Q 2 ) (i = 1 , 2 i = 1, 2 i = 1 , 2 )で、右辺 ⊆ \subseteq ⊆ 左辺が分かります(この向きに決定性は要りません)。逆を示します。σ ∈ w p ( c , Q 1 ∨ Q 2 ) \sigma \in \mathrm{wp}(c, Q_1 \vee Q_2) σ ∈ wp ( c , Q 1 ∨ Q 2 ) とします。c c c は σ \sigma σ から停止するので ⟨ c , σ ⟩ ⇓ σ ′ \langle c, \sigma \rangle \Downarrow \sigma' ⟨ c , σ ⟩ ⇓ σ ′ となる σ ′ \sigma' σ ′ があり、補題 3.2 によりそのような σ ′ \sigma' σ ′ はただ 1 つです。Q 1 ( σ ′ ) Q_1(\sigma') Q 1 ( σ ′ ) または Q 2 ( σ ′ ) Q_2(\sigma') Q 2 ( σ ′ ) が成り立ちます。Q 1 ( σ ′ ) Q_1(\sigma') Q 1 ( σ ′ ) なら、実行の結果は σ ′ \sigma' σ ′ だけなので「すべての結果が Q 1 Q_1 Q 1 を満たす」ことになり σ ∈ w p ( c , Q 1 ) \sigma \in \mathrm{wp}(c, Q_1) σ ∈ wp ( c , Q 1 ) です。Q 2 ( σ ′ ) Q_2(\sigma') Q 2 ( σ ′ ) なら同様に σ ∈ w p ( c , Q 2 ) \sigma \in \mathrm{wp}(c, Q_2) σ ∈ wp ( c , Q 2 ) です。w l p \mathrm{wlp} wlp については、c c c が σ \sigma σ から停止しない場合に σ \sigma σ が w l p ( c , Q 1 ) \mathrm{wlp}(c, Q_1) wlp ( c , Q 1 ) に(条件が空虚に成り立って)属することを加えれば、同じ議論です。
同値の主張。⇐ \Leftarrow ⇐ は (1) で示しました。⊨ [ P ] c [ Q ] \models [P]\ c\ [Q] ⊨ [ P ] c [ Q ] とし、P ( σ ) P(\sigma) P ( σ ) とします。⟨ c , σ ⟩ ⇓ σ ′ \langle c, \sigma \rangle \Downarrow \sigma' ⟨ c , σ ⟩ ⇓ σ ′ かつ Q ( σ ′ ) Q(\sigma') Q ( σ ′ ) となる σ ′ \sigma' σ ′ があります。c c c は σ \sigma σ から停止し、補題 3.2 により結果は σ ′ \sigma' σ ′ だけなので、すべての結果が Q Q Q を満たします。よって σ ∈ w p ( c , Q ) \sigma \in \mathrm{wp}(c, Q) σ ∈ wp ( c , Q ) です。
(5) s k i p \mathtt{skip} skip 。⟨ s k i p , σ ⟩ ⇓ σ ′ \langle \mathtt{skip}, \sigma \rangle \Downarrow \sigma' ⟨ skip , σ ⟩ ⇓ σ ′ は σ ′ = σ \sigma' = \sigma σ ′ = σ と同値(規則は (Skip) だけ)なので、σ ∈ w l p ( s k i p , Q ) \sigma \in \mathrm{wlp}(\mathtt{skip}, Q) σ ∈ wlp ( skip , Q ) は Q ( σ ) Q(\sigma) Q ( σ ) と同値です。
代入。命題 3.6 の (1) と (1) の特徴づけから Q [ a / x ] ⇒ w l p ( x : = a , Q ) Q[a/x] \Rightarrow \mathrm{wlp}(x := a, Q) Q [ a / x ] ⇒ wlp ( x := a , Q ) 。逆に (1) より ⊨ { w l p ( x : = a , Q ) } x : = a { Q } \models \{\mathrm{wlp}(x := a, Q)\}\ x := a\ \{Q\} ⊨ { wlp ( x := a , Q )} x := a { Q } なので、命題 3.6 の (2) から w l p ( x : = a , Q ) ⇒ Q [ a / x ] \mathrm{wlp}(x := a, Q) \Rightarrow Q[a/x] wlp ( x := a , Q ) ⇒ Q [ a / x ] です。
逐次。⟨ c 1 ; c 2 , σ ⟩ ⇓ σ ′ ′ \langle c_1; c_2, \sigma \rangle \Downarrow \sigma'' ⟨ c 1 ; c 2 , σ ⟩ ⇓ σ ′′ は、「⟨ c 1 , σ ⟩ ⇓ σ 1 \langle c_1, \sigma \rangle \Downarrow \sigma_1 ⟨ c 1 , σ ⟩ ⇓ σ 1 かつ ⟨ c 2 , σ 1 ⟩ ⇓ σ ′ ′ \langle c_2, \sigma_1 \rangle \Downarrow \sigma'' ⟨ c 2 , σ 1 ⟩ ⇓ σ ′′ となる σ 1 \sigma_1 σ 1 がある」ことと同値です(⇒ \Rightarrow ⇒ は、この形を結論にもつ規則が (Seq) だけであること、⇐ \Leftarrow ⇐ は (Seq) そのもの)。したがって
σ ∈ w l p ( c 1 ; c 2 , Q ) ⟺ ∀ σ 1 ∀ σ ′ ′ ( ⟨ c 1 , σ ⟩ ⇓ σ 1 かつ ⟨ c 2 , σ 1 ⟩ ⇓ σ ′ ′ ⇒ Q ( σ ′ ′ ) ) ⟺ ∀ σ 1 ( ⟨ c 1 , σ ⟩ ⇓ σ 1 ⇒ σ 1 ∈ w l p ( c 2 , Q ) ) ⟺ σ ∈ w l p ( c 1 , w l p ( c 2 , Q ) ) \begin{aligned}
\sigma \in \mathrm{wlp}(c_1; c_2, Q)
&\iff \forall \sigma_1\ \forall \sigma''\ \bigl(\langle c_1, \sigma \rangle \Downarrow \sigma_1 \text{ かつ } \langle c_2, \sigma_1 \rangle \Downarrow \sigma'' \Rightarrow Q(\sigma'')\bigr) \\
&\iff \forall \sigma_1\ \bigl(\langle c_1, \sigma \rangle \Downarrow \sigma_1 \Rightarrow \sigma_1 \in \mathrm{wlp}(c_2, Q)\bigr) \\
&\iff \sigma \in \mathrm{wlp}(c_1, \mathrm{wlp}(c_2, Q))
\end{aligned} σ ∈ wlp ( c 1 ; c 2 , Q ) ⟺ ∀ σ 1 ∀ σ ′′ ( ⟨ c 1 , σ ⟩ ⇓ σ 1 かつ ⟨ c 2 , σ 1 ⟩ ⇓ σ ′′ ⇒ Q ( σ ′′ ) ) ⟺ ∀ σ 1 ( ⟨ c 1 , σ ⟩ ⇓ σ 1 ⇒ σ 1 ∈ wlp ( c 2 , Q ) ) ⟺ σ ∈ wlp ( c 1 , wlp ( c 2 , Q )) です。
条件分岐。⟦ b ⟧ σ = t t \llbracket b \rrbracket\sigma = \mathrm{tt} [ [ b ] ] σ = tt のとき、条件分岐の実行の導出は (IfT) でしか終われないので、⟨ i f b t h e n c 1 e l s e c 2 , σ ⟩ ⇓ σ ′ \langle \mathtt{if}\ b\ \mathtt{then}\ c_1\ \mathtt{else}\ c_2, \sigma \rangle \Downarrow \sigma' ⟨ if b then c 1 else c 2 , σ ⟩ ⇓ σ ′ は ⟨ c 1 , σ ⟩ ⇓ σ ′ \langle c_1, \sigma \rangle \Downarrow \sigma' ⟨ c 1 , σ ⟩ ⇓ σ ′ と同値です。よってこのような σ \sigma σ については、左辺に属することと σ ∈ w l p ( c 1 , Q ) \sigma \in \mathrm{wlp}(c_1, Q) σ ∈ wlp ( c 1 , Q ) が同値で、右辺に属すること(¬ b \lnot b ¬ b の側は成り立たないので σ ∈ w l p ( c 1 , Q ) \sigma \in \mathrm{wlp}(c_1, Q) σ ∈ wlp ( c 1 , Q ) )とも同値です。⟦ b ⟧ σ = f f \llbracket b \rrbracket\sigma = \mathrm{ff} [ [ b ] ] σ = ff のときは c 2 c_2 c 2 について同じです。
∎
定理の五つの性質のうち、実際に計算で使うのは (1) と (5) です。(2)〜(4) は、条件を足したり弱めたりしても振る舞いが素直に変わることの確認です。先に次の具体例を見てから読むと分かりやすくなります。
(1) が名前の由来です。w l p ( c , Q ) \mathrm{wlp}(c, Q) wlp ( c , Q ) は { P } c { Q } \{P\}\ c\ \{Q\} { P } c { Q } を部分正当にする P P P のうちでもっとも弱いものです。(5) の計算規則を後ろから順に使うと、ループを含まない命令の w l p \mathrm{wlp} wlp は機械的に計算できます。例 3.10 の検証条件は、この計算の結果です。検証ツールが「ループ不変条件だけ人が書けば、あとは自動」と言えるのは、この計算規則と (1) によります。
例 4.3 (絶対値の計算の最弱事前条件 )
c c c を i f x < 0 t h e n y : = 0 − x e l s e y : = x \mathtt{if}\ x \lt 0\ \mathtt{then}\ y := 0 - x\ \mathtt{else}\ y := x if x < 0 then y := 0 − x else y := x とし、事後条件 Q Q Q を y ≥ 0 y \ge 0 y ≥ 0 とします。定理 4.2 (5) より
w l p ( c , Q ) = ( x < 0 ∧ Q [ 0 − x / y ] ) ∨ ( ¬ ( x < 0 ) ∧ Q [ x / y ] ) = ( x < 0 ∧ 0 − x ≥ 0 ) ∨ ( x ≥ 0 ∧ x ≥ 0 ) . \mathrm{wlp}(c, Q) = \bigl(x \lt 0 \wedge Q[0 - x/y]\bigr) \vee \bigl(\lnot(x \lt 0) \wedge Q[x/y]\bigr) = (x \lt 0 \wedge 0 - x \ge 0) \vee (x \ge 0 \wedge x \ge 0). wlp ( c , Q ) = ( x < 0 ∧ Q [ 0 − x / y ] ) ∨ ( ¬ ( x < 0 ) ∧ Q [ x / y ] ) = ( x < 0 ∧ 0 − x ≥ 0 ) ∨ ( x ≥ 0 ∧ x ≥ 0 ) . x < 0 x \lt 0 x < 0 なら − x ≥ 0 -x \ge 0 − x ≥ 0 なので 1 つ目は x < 0 x \lt 0 x < 0 、2 つ目は x ≥ 0 x \ge 0 x ≥ 0 で、選言は t r u e \mathrm{true} true です。事前条件なしで y ≥ 0 y \ge 0 y ≥ 0 が保証されます。
誤って e l s e \mathtt{else} else の枝も y : = 0 − x y := 0 - x y := 0 − x と書いた命令 c ′ c' c ′ では、2 つ目が x ≥ 0 ∧ 0 − x ≥ 0 x \ge 0 \wedge 0 - x \ge 0 x ≥ 0 ∧ 0 − x ≥ 0 、すなわち x = 0 x = 0 x = 0 になり、w l p ( c ′ , Q ) = ( x ≤ 0 ) \mathrm{wlp}(c', Q) = (x \le 0) wlp ( c ′ , Q ) = ( x ≤ 0 ) です。誤った実装が契約を満たす入力の範囲 が、式として得られました。x ≤ 0 x \le 0 x ≤ 0 の入力だけでテストすると、この誤りは見つかりません。
例 4.4 (非決定的な操作では選言と可換でない )
定理 4.2 (4) の仮定は落とせません。操作 m m m の解釈を M ( m ) = { ( σ , σ [ x ↦ 0 ] ) ∣ σ ∈ Σ } ∪ { ( σ , σ [ x ↦ 1 ] ) ∣ σ ∈ Σ } \mathcal{M}(m) = \{(\sigma, \sigma[x \mapsto 0]) \mid \sigma \in \Sigma\} \cup \{(\sigma, \sigma[x \mapsto 1]) \mid \sigma \in \Sigma\} M ( m ) = {( σ , σ [ x ↦ 0 ]) ∣ σ ∈ Σ } ∪ {( σ , σ [ x ↦ 1 ]) ∣ σ ∈ Σ } (x x x に 0 0 0 か 1 1 1 のどちらかを入れる)とします。c a l l m \mathtt{call}\ m call m はどの状態からも停止し、結果は必ず x = 0 ∨ x = 1 x = 0 \vee x = 1 x = 0 ∨ x = 1 を満たすので、w p ( c a l l m , x = 0 ∨ x = 1 ) = t r u e \mathrm{wp}(\mathtt{call}\ m, x = 0 \vee x = 1) = \mathrm{true} wp ( call m , x = 0 ∨ x = 1 ) = true です。一方、どの状態からも x = 1 x = 1 x = 1 となる実行があるので w p ( c a l l m , x = 0 ) = f a l s e \mathrm{wp}(\mathtt{call}\ m, x = 0) = \mathrm{false} wp ( call m , x = 0 ) = false 、同様に w p ( c a l l m , x = 1 ) = f a l s e \mathrm{wp}(\mathtt{call}\ m, x = 1) = \mathrm{false} wp ( call m , x = 1 ) = false で、選言は f a l s e \mathrm{false} false です。並行プログラムや、仕様が複数の結果を許す操作では、この違いが現れます。
(H-While) は、ループが止まることを何も保証しません。停止性を示すには、反復のたびに「減っていく量」を見つけます。
定義 5.1 (ループ変量 ) ↗
ループ w h i l e b d o c \mathtt{while}\ b\ \mathtt{do}\ c while b do c と表明 I I I に対し、写像 v : Σ → Z v : \Sigma \to \mathbb{Z} v : Σ → Z が次の 2 条件を満たすとき、v v v を(不変条件 I I I のもとでの)ループ変量 という。
(下に有界)I ( σ ) I(\sigma) I ( σ ) かつ ⟦ b ⟧ σ = t t \llbracket b \rrbracket\sigma = \mathrm{tt} [ [ b ] ] σ = tt ならば v ( σ ) ≥ 0 v(\sigma) \ge 0 v ( σ ) ≥ 0 。
(真に減る)すべての k ∈ Z k \in \mathbb{Z} k ∈ Z について ⊨ [ I ∧ b ∧ v = k ] c [ I ∧ v < k ] \models [I \wedge b \wedge v = k]\ c\ [I \wedge v \lt k] ⊨ [ I ∧ b ∧ v = k ] c [ I ∧ v < k ] 。ここで v = k v = k v = k は表明 { σ ∣ v ( σ ) = k } \{\sigma \mid v(\sigma) = k\} { σ ∣ v ( σ ) = k } 、v < k v \lt k v < k は { σ ∣ v ( σ ) < k } \{\sigma \mid v(\sigma) \lt k\} { σ ∣ v ( σ ) < k } を表す。
2 つ目の条件は、本体 c c c が停止すること、不変条件 I I I を保つこと、変量を真に減らすことを 1 つの全正当な三つ組にまとめたものです。
次の定理の証明の方針は、背理法です。「止まらない状態」があるとして、その中で変量が最小のものを取り出し、本体を 1 回実行すると、変量がもっと小さい状態に移ることを使って矛盾を導きます。自然数には最小元があるので、このように「最小の悪い例」を取れます。
定理 5.2 (変量による停止性:全正当性の規則の健全性 ) ✓
M \mathcal{M} M を解釈とする。
ループ w h i l e b d o c \mathtt{while}\ b\ \mathtt{do}\ c while b do c が、表明 I I I のもとでループ変量 v v v をもつならば、⊨ M [ I ] w h i l e b d o c [ I ∧ ¬ b ] \models_{\mathcal{M}} [I]\ \mathtt{while}\ b\ \mathtt{do}\ c\ [I \wedge \lnot b] ⊨ M [ I ] while b do c [ I ∧ ¬ b ] である。
c a l l \mathtt{call} call を含まない命令について、定義 3.7 の (H-Skip)・(H-Assign)・(H-Seq)・(H-If)・(H-Conseq) は、三つ組を全正当性 [ P ] c [ Q ] [P]\ c\ [Q] [ P ] c [ Q ] に読み替えても正しい。すなわち、各規則の前提の三つ組がすべて全正当ならば、結論の三つ組も全正当である。
証明(定理 5.2) (1) w = w h i l e b d o c w = \mathtt{while}\ b\ \mathtt{do}\ c w = while b do c とおき、状態 σ \sigma σ が良い とは「⟨ w , σ ⟩ ⇓ σ ′ ′ \langle w, \sigma \rangle \Downarrow \sigma'' ⟨ w , σ ⟩ ⇓ σ ′′ 、I ( σ ′ ′ ) I(\sigma'') I ( σ ′′ ) 、⟦ b ⟧ σ ′ ′ = f f \llbracket b \rrbracket\sigma'' = \mathrm{ff} [ [ b ] ] σ ′′ = ff を満たす σ ′ ′ \sigma'' σ ′′ が存在する」ことと定めます。I I I を満たす状態がすべて良いことを示せば、それが結論です。
まず、I ( σ ) I(\sigma) I ( σ ) かつ ⟦ b ⟧ σ = f f \llbracket b \rrbracket\sigma = \mathrm{ff} [ [ b ] ] σ = ff である σ \sigma σ は良い状態です。(WhF) より ⟨ w , σ ⟩ ⇓ σ \langle w, \sigma \rangle \Downarrow \sigma ⟨ w , σ ⟩ ⇓ σ で、σ ′ ′ = σ \sigma'' = \sigma σ ′′ = σ が条件を満たすからです。
背理法で、I I I を満たすのに良くない状態があると仮定します。そのような状態の全体を B B B とします。いま見たことから、B B B の元 σ \sigma σ では ⟦ b ⟧ σ = t t \llbracket b \rrbracket\sigma = \mathrm{tt} [ [ b ] ] σ = tt で、変量の 1 つ目の条件から v ( σ ) ≥ 0 v(\sigma) \ge 0 v ( σ ) ≥ 0 です。したがって
S = { v ( σ ) + 1 ∣ σ ∈ B } S = \{\, v(\sigma) + 1 \mid \sigma \in B \,\} S = { v ( σ ) + 1 ∣ σ ∈ B } は N = { 1 , 2 , … } \mathbb{N} = \{1, 2, \ldots\} N = { 1 , 2 , … } の空でない部分集合です。整列性(公理 3.1[証明の技術] )により S S S は最小元をもつので、v ( σ ) + 1 v(\sigma) + 1 v ( σ ) + 1 がその最小元になる σ ∈ B \sigma \in B σ ∈ B をとります。
k = v ( σ ) k = v(\sigma) k = v ( σ ) とおくと ( I ∧ b ∧ v = k ) ( σ ) (I \wedge b \wedge v = k)(\sigma) ( I ∧ b ∧ v = k ) ( σ ) なので、変量の 2 つ目の条件から、⟨ c , σ ⟩ ⇓ σ 1 \langle c, \sigma \rangle \Downarrow \sigma_1 ⟨ c , σ ⟩ ⇓ σ 1 、I ( σ 1 ) I(\sigma_1) I ( σ 1 ) 、v ( σ 1 ) < k v(\sigma_1) \lt k v ( σ 1 ) < k を満たす σ 1 \sigma_1 σ 1 があります。σ 1 \sigma_1 σ 1 は良い状態です。実際、⟦ b ⟧ σ 1 = f f \llbracket b \rrbracket\sigma_1 = \mathrm{ff} [ [ b ] ] σ 1 = ff なら先に見たとおり良く、⟦ b ⟧ σ 1 = t t \llbracket b \rrbracket\sigma_1 = \mathrm{tt} [ [ b ] ] σ 1 = tt のとき、もし σ 1 ∈ B \sigma_1 \in B σ 1 ∈ B なら v ( σ 1 ) + 1 ∈ S v(\sigma_1) + 1 \in S v ( σ 1 ) + 1 ∈ S かつ v ( σ 1 ) + 1 < v ( σ ) + 1 v(\sigma_1) + 1 \lt v(\sigma) + 1 v ( σ 1 ) + 1 < v ( σ ) + 1 となって最小性に反するからです。
σ 1 \sigma_1 σ 1 が良いので、⟨ w , σ 1 ⟩ ⇓ σ ′ ′ \langle w, \sigma_1 \rangle \Downarrow \sigma'' ⟨ w , σ 1 ⟩ ⇓ σ ′′ 、I ( σ ′ ′ ) I(\sigma'') I ( σ ′′ ) 、⟦ b ⟧ σ ′ ′ = f f \llbracket b \rrbracket\sigma'' = \mathrm{ff} [ [ b ] ] σ ′′ = ff となる σ ′ ′ \sigma'' σ ′′ があります。⟦ b ⟧ σ = t t \llbracket b \rrbracket\sigma = \mathrm{tt} [ [ b ] ] σ = tt 、⟨ c , σ ⟩ ⇓ σ 1 \langle c, \sigma \rangle \Downarrow \sigma_1 ⟨ c , σ ⟩ ⇓ σ 1 、⟨ w , σ 1 ⟩ ⇓ σ ′ ′ \langle w, \sigma_1 \rangle \Downarrow \sigma'' ⟨ w , σ 1 ⟩ ⇓ σ ′′ に (WhT) を使うと ⟨ w , σ ⟩ ⇓ σ ′ ′ \langle w, \sigma \rangle \Downarrow \sigma'' ⟨ w , σ ⟩ ⇓ σ ′′ です。これは σ \sigma σ が良いことを意味し、σ ∈ B \sigma \in B σ ∈ B に反します。よって B B B は空です。
(2) 各規則を順に確かめます。
(H-Skip)。P ( σ ) P(\sigma) P ( σ ) とします。(Skip) より ⟨ s k i p , σ ⟩ ⇓ σ \langle \mathtt{skip}, \sigma \rangle \Downarrow \sigma ⟨ skip , σ ⟩ ⇓ σ で、P ( σ ) P(\sigma) P ( σ ) です。
(H-Assign)。Q [ a / x ] ( σ ) Q[a/x](\sigma) Q [ a / x ] ( σ ) とします。σ ′ = σ [ x ↦ ⟦ a ⟧ σ ] \sigma' = \sigma[x \mapsto \llbracket a \rrbracket\sigma] σ ′ = σ [ x ↦ [ [ a ] ] σ ] とおくと (Assign) より ⟨ x : = a , σ ⟩ ⇓ σ ′ \langle x := a, \sigma \rangle \Downarrow \sigma' ⟨ x := a , σ ⟩ ⇓ σ ′ で、定義 3.4 より Q ( σ ′ ) Q(\sigma') Q ( σ ′ ) です。
(H-Seq)。⊨ [ P ] c 1 [ R ] \models [P]\ c_1\ [R] ⊨ [ P ] c 1 [ R ] 、⊨ [ R ] c 2 [ Q ] \models [R]\ c_2\ [Q] ⊨ [ R ] c 2 [ Q ] とし、P ( σ ) P(\sigma) P ( σ ) とします。1 つ目から ⟨ c 1 , σ ⟩ ⇓ σ 1 \langle c_1, \sigma \rangle \Downarrow \sigma_1 ⟨ c 1 , σ ⟩ ⇓ σ 1 かつ R ( σ 1 ) R(\sigma_1) R ( σ 1 ) となる σ 1 \sigma_1 σ 1 があり、2 つ目から ⟨ c 2 , σ 1 ⟩ ⇓ σ ′ \langle c_2, \sigma_1 \rangle \Downarrow \sigma' ⟨ c 2 , σ 1 ⟩ ⇓ σ ′ かつ Q ( σ ′ ) Q(\sigma') Q ( σ ′ ) となる σ ′ \sigma' σ ′ があります。(Seq) より ⟨ c 1 ; c 2 , σ ⟩ ⇓ σ ′ \langle c_1; c_2, \sigma \rangle \Downarrow \sigma' ⟨ c 1 ; c 2 , σ ⟩ ⇓ σ ′ です。
(H-If)。⊨ [ P ∧ b ] c 1 [ Q ] \models [P \wedge b]\ c_1\ [Q] ⊨ [ P ∧ b ] c 1 [ Q ] 、⊨ [ P ∧ ¬ b ] c 2 [ Q ] \models [P \wedge \lnot b]\ c_2\ [Q] ⊨ [ P ∧ ¬ b ] c 2 [ Q ] とし、P ( σ ) P(\sigma) P ( σ ) とします。⟦ b ⟧ σ = t t \llbracket b \rrbracket\sigma = \mathrm{tt} [ [ b ] ] σ = tt なら 1 つ目から ⟨ c 1 , σ ⟩ ⇓ σ ′ \langle c_1, \sigma \rangle \Downarrow \sigma' ⟨ c 1 , σ ⟩ ⇓ σ ′ かつ Q ( σ ′ ) Q(\sigma') Q ( σ ′ ) となる σ ′ \sigma' σ ′ があり、(IfT) より条件分岐も σ \sigma σ から σ ′ \sigma' σ ′ に至ります。f f \mathrm{ff} ff なら 2 つ目と (IfF) で同じです。
(H-Conseq)。P ⇒ P ′ P \Rightarrow P' P ⇒ P ′ 、⊨ [ P ′ ] c [ Q ′ ] \models [P']\ c\ [Q'] ⊨ [ P ′ ] c [ Q ′ ] 、Q ′ ⇒ Q Q' \Rightarrow Q Q ′ ⇒ Q とし、P ( σ ) P(\sigma) P ( σ ) とします。P ′ ( σ ) P'(\sigma) P ′ ( σ ) なので ⟨ c , σ ⟩ ⇓ σ ′ \langle c, \sigma \rangle \Downarrow \sigma' ⟨ c , σ ⟩ ⇓ σ ′ かつ Q ′ ( σ ′ ) Q'(\sigma') Q ′ ( σ ′ ) となる σ ′ \sigma' σ ′ があり、Q ( σ ′ ) Q(\sigma') Q ( σ ′ ) です。
∎
証明の核は、「良くない状態があれば、変量が最小のものをとれる」という 1 点です。変量の値が整礎 な集合(無限に下がり続ける列が無い集合)に入っていることが効いています。値が有理数だと、この議論は使えません。有理数の変数 x x x について「x > 0 x \gt 0 x > 0 のあいだ x x x を半分にする」ループは、x x x が毎回真に減り 0 0 0 以上にとどまりますが、止まりません。1 , 1 / 2 , 1 / 4 , … 1, 1/2, 1/4, \ldots 1 , 1/2 , 1/4 , … は、下に有界でも最小元に到達しないからです。
例 5.3 (二分探索の変量 )
例 3.10 のループに、変量 v = h i − l o v = hi - lo v = hi − l o をとります。I ∧ l o < h i I \wedge lo \lt hi I ∧ l o < hi のもとで v ≥ 1 ≥ 0 v \ge 1 \ge 0 v ≥ 1 ≥ 0 です。本体は代入と条件分岐だけなので必ず停止し、I I I を保つことは検証条件 (V2) で見ました。l o ≤ m i d < h i lo \le mid \lt hi l o ≤ mi d < hi なので、l o : = m i d + 1 lo := mid + 1 l o := mi d + 1 のあとの v v v は h i − m i d − 1 < h i − l o hi - mid - 1 \lt hi - lo hi − mi d − 1 < hi − l o 、h i : = m i d hi := mid hi := mi d のあとの v v v は m i d − l o < h i − l o mid - lo \lt hi - lo mi d − l o < hi − l o で、どちらも真に減ります。定理 5.2 (1) と、初期化の部分への (2) の適用から、プログラム全体が全正当です。これは 定理 3.2[探索アルゴリズム] の「必ず停止し、下限位置を返す」と同じ主張です。変量は「あと何回まわりうるか」の上界でもあり、毎回 1 1 1 以上減る非負整数なので、反復は高々 n n n 回です。実際には半分以下に減ることを使うと ⌊ log 2 n ⌋ + 1 \lfloor \log_2 n \rfloor + 1 ⌊ log 2 n ⌋ + 1 回という評価が得られます(定理 3.3[探索アルゴリズム] )。
例 5.4 (中点の計算と値の範囲 )
言語 W の変数は任意の整数をとりますが、多くの言語の整数型は範囲が有限です。32 ビットの符号つき整数は − 2 31 -2^{31} − 2 31 から 2 31 − 1 = 2147483647 2^{31} - 1 = 2147483647 2 31 − 1 = 2147483647 までで、範囲を超えた加算の結果は 2 32 2^{32} 2 32 を法として折り返します(言語によっては未定義の動作です)。中点を ( l o + h i ) ÷ 2 (lo + hi) \div 2 ( l o + hi ) ÷ 2 と書いた二分探索で、l o = 2 30 = 1073741824 lo = 2^{30} = 1073741824 l o = 2 30 = 1073741824 、h i = 2 30 + 2 = 1073741826 hi = 2^{30} + 2 = 1073741826 hi = 2 30 + 2 = 1073741826 になったとします。数学的な和は 2147483650 2147483650 2147483650 で、2 31 − 1 2^{31} - 1 2 31 − 1 を 3 3 3 超えます。折り返した値は 2147483650 − 2 32 = − 2147483646 2147483650 - 2^{32} = -2147483646 2147483650 − 2 32 = − 2147483646 、その半分は − 1073741823 -1073741823 − 1073741823 で、添字として範囲外です。
W の上での証明は、この実装の正しさを保証しません。証明した対象(整数が無限にある言語のプログラム)と、動かす対象(32 ビットで折り返す機械の上のプログラム)が違うからです。契約に値の範囲 を入れると食い違いが見えます。「すべての中間結果が 2 31 − 1 2^{31} - 1 2 31 − 1 以下」を不変条件に足そうとすると、l o + h i ≤ 2 n − 1 lo + hi \le 2n - 1 l o + hi ≤ 2 n − 1 までしか言えないので、事前条件に n ≤ 2 30 n \le 2^{30} n ≤ 2 30 が必要になります。一方 l o + ( h i − l o ) ÷ 2 lo + (hi - lo) \div 2 l o + ( hi − l o ) ÷ 2 なら、中間結果は h i − l o ≤ n hi - lo \le n hi − l o ≤ n と m i d < h i ≤ n mid \lt hi \le n mi d < hi ≤ n で、事前条件は n ≤ 2 31 − 1 n \le 2^{31} - 1 n ≤ 2 31 − 1 で足ります。契約は、どのモデルの上の主張なのかまで含めて書いて、はじめて実装を縛ります。
ここまでは 1 つの手続きの契約でした。クラス(抽象データ型 )は、内部の状態を複数の操作が共有します。操作ごとの事前・事後条件に加えて、「操作と操作のあいだで、内部の状態がいつも満たしている条件」が要ります。
定義 6.1 (データ型の実装・表現不変条件・抽象化関数 ) ↗
データ型の実装 とは、次の組 ( R e p , I n i t , ( o p m ) m ∈ O p ) (\mathrm{Rep}, \mathrm{Init}, (\mathrm{op}_m)_{m \in \mathrm{Op}}) ( Rep , Init , ( op m ) m ∈ Op ) をいう。
R e p \mathrm{Rep} Rep は表現 (内部の状態)の集合、I n i t ⊆ R e p \mathrm{Init} \subseteq \mathrm{Rep} Init ⊆ Rep は構築子が作りうる表現の集合。
各公開操作 m ∈ O p m \in \mathrm{Op} m ∈ Op は、引数の集合 X m X_m X m 、結果の集合 Y m Y_m Y m 、事前条件 p r e m ⊆ R e p × X m \mathrm{pre}_m \subseteq \mathrm{Rep} \times X_m pre m ⊆ Rep × X m と、写像 o p m : R e p × X m → R e p × Y m \mathrm{op}_m : \mathrm{Rep} \times X_m \to \mathrm{Rep} \times Y_m op m : Rep × X m → Rep × Y m (新しい表現と結果を返す)をもつ。
表現不変条件 とは、部分集合 I n v ⊆ R e p \mathrm{Inv} \subseteq \mathrm{Rep} Inv ⊆ Rep をいう。集合 A A A (抽象値 の集合)への写像 α : I n v → A \alpha : \mathrm{Inv} \to A α : Inv → A を抽象化関数 という。
このデータ型を使うプログラムの実行 とは、表現と呼び出し側の状態の組の列 ( r 0 , e 0 ) , ( r 1 , e 1 ) , … , ( r N , e N ) (r_0, e_0), (r_1, e_1), \ldots, (r_N, e_N) ( r 0 , e 0 ) , ( r 1 , e 1 ) , … , ( r N , e N ) をいう。実行が表現を漏らさない とは、r 0 ∈ I n i t r_0 \in \mathrm{Init} r 0 ∈ Init であり、各 i < N i \lt N i < N について次のどちらかが成り立つことをいう。
(公開操作の呼び出し)ある m ∈ O p m \in \mathrm{Op} m ∈ Op と x ∈ X m x \in X_m x ∈ X m があって、( r i , x ) ∈ p r e m (r_i, x) \in \mathrm{pre}_m ( r i , x ) ∈ pre m かつ o p m ( r i , x ) = ( r i + 1 , y ) \mathrm{op}_m(r_i, x) = (r_{i+1}, y) op m ( r i , x ) = ( r i + 1 , y ) (y y y は結果)。
(呼び出し側の動作)r i + 1 = r i r_{i+1} = r_i r i + 1 = r i 。
「表現を漏らさない」は、表現が変わるのは公開操作を通したときだけ、という条件です。呼び出し側が自分の状態 e e e をどう変えようと、表現 r r r には触れられません。プログラミング言語では、フィールドを非公開にし、内部の可変なオブジェクトへの参照(別名)を外に渡さないことで、この条件を成り立たせます。
定理 6.2 (表現不変条件の帰納法 ) ✓
データ型の実装と表現不変条件 I n v \mathrm{Inv} Inv が次の 2 条件を満たすとする。
(成立)I n i t ⊆ I n v \mathrm{Init} \subseteq \mathrm{Inv} Init ⊆ Inv 。
(保存)すべての m ∈ O p m \in \mathrm{Op} m ∈ Op 、r ∈ I n v r \in \mathrm{Inv} r ∈ Inv 、x ∈ X m x \in X_m x ∈ X m について、( r , x ) ∈ p r e m (r, x) \in \mathrm{pre}_m ( r , x ) ∈ pre m かつ o p m ( r , x ) = ( r ′ , y ) \mathrm{op}_m(r, x) = (r', y) op m ( r , x ) = ( r ′ , y ) ならば r ′ ∈ I n v r' \in \mathrm{Inv} r ′ ∈ Inv 。
このとき、表現を漏らさない任意の実行 ( r 0 , e 0 ) , … , ( r N , e N ) (r_0, e_0), \ldots, (r_N, e_N) ( r 0 , e 0 ) , … , ( r N , e N ) について、すべての i i i で r i ∈ I n v r_i \in \mathrm{Inv} r i ∈ Inv である。
証明(定理 6.2) i i i についての帰納法(定理 3.2[証明の技術] 。i = 0 i = 0 i = 0 から始めます)で示します。
i = 0 i = 0 i = 0 。表現を漏らさない実行の定義から r 0 ∈ I n i t r_0 \in \mathrm{Init} r 0 ∈ Init で、(成立)より r 0 ∈ I n v r_0 \in \mathrm{Inv} r 0 ∈ Inv です。
i < N i \lt N i < N とし、r i ∈ I n v r_i \in \mathrm{Inv} r i ∈ Inv と仮定します。定義から、i i i 番目の段は 2 種類のどちらかです。公開操作の呼び出しなら、( r i , x ) ∈ p r e m (r_i, x) \in \mathrm{pre}_m ( r i , x ) ∈ pre m かつ o p m ( r i , x ) = ( r i + 1 , y ) \mathrm{op}_m(r_i, x) = (r_{i+1}, y) op m ( r i , x ) = ( r i + 1 , y ) で、r i ∈ I n v r_i \in \mathrm{Inv} r i ∈ Inv なので(保存)より r i + 1 ∈ I n v r_{i+1} \in \mathrm{Inv} r i + 1 ∈ Inv です。呼び出し側の動作なら r i + 1 = r i ∈ I n v r_{i+1} = r_i \in \mathrm{Inv} r i + 1 = r i ∈ Inv です。
∎
証明は短いですが、使い道は大きい定理です。(保存)を確かめるとき、各操作は入口で不変条件を仮定してよい 。I n v \mathrm{Inv} Inv は、操作から見れば「いつでも使ってよい事前条件」であり、同時に「必ず守るべき事後条件」です。これは (H-While) と同じ形をしています。オブジェクトの一生を「公開操作を任意の順で繰り返すループ」とみれば、表現不変条件はそのループ不変条件です。
定理の 3 つの仮定(成立・保存・漏らさない)は、どれも落とせません。3 つ目が破れる例を実行して確かめます。
例 6.3 (表現が漏れると不変条件が破れる )
整列済みのリストで集合を表すクラスです。表現不変条件は「_xs は狭義昇順」で、add と in はこれを前提に二分探索を使います。
# Python 3.12.14(標準ライブラリのみ)
""" 表現: リスト _xs。表現不変条件: _xs は狭義昇順(整列済みで重複がない)。 """
return all ( self ._xs [ i ] < self ._xs [ i + 1 ] for i in range ( len ( self ._xs ) - 1 ))
i = bisect. bisect_left ( self ._xs , x ) # 不変条件を使う(二分探索)
if i == len ( self ._xs ) or self ._xs[i] != x:
def __contains__ ( self , x ) :
i = bisect. bisect_left ( self ._xs , x ) # 不変条件を使う
return i < len ( self ._xs ) and self ._xs[i] == x
return self ._xs # 表現そのもの(別名)を返す
return list ( self ._xs ) # コピーを返す
for getter in ( " items_leaky " , " items " ):
view = getattr ( s , getter )()
view. append ( 0 ) # 呼び出し側が受け取ったリストを書き換える
print ( f " {getter :12s } 表現 = {s._xs} 不変条件 = {s. _inv () } 0 in s = { 0 in s} 5 in s = { 5 in s} " )
items_leaky 表現 = [1, 2, 3, 5, 0] 不変条件 = False 0 in s = False 5 in s = False
items 表現 = [1, 2, 3, 5] 不変条件 = True 0 in s = False 5 in s = True
items_leaky は内部のリストそのものを返すので、呼び出し側の view.append(0) が表現を直接書き換えます。これは 定義 6.1 の 2 種類の段のどちらでもありません(公開操作でないのに r r r が変わっています)。不変条件が破れた結果、add で入れたはずの 5 が「入っていない」と答えています。二分探索が、整列を前提に 5 の位置を通り過ぎるからです。壊れたのは items_leaky を呼んだ時点ではなく、あとで別の操作を呼んだとき で、原因と症状が離れます。items はコピーを返すので、呼び出し側が何をしても表現は変わらず、定理 6.2 の仮定が保たれます。
不変条件は「表現が壊れていない」ことを言うだけで、操作が仕様どおりの答えを返すことまでは言いません。それを言うのが抽象化関数です。
r(表現) r’(表現) α(r)(抽象値) α(r’)(抽象値) 実装の操作 op(結果 y) 仕様の操作 opᴬ(結果 y) α α 抽象化関数 α と操作の可換性。実装の側で操作してから抽象化しても、抽象化してから仕様の側で操作しても、同じ抽象値と同じ結果 y になる。
命題 6.4 (抽象化関数が操作と可換なら実装は仕様を満たす ) ✓
抽象仕様 として、抽象値の集合 A A A 、初期値 a 0 ∈ A a_0 \in A a 0 ∈ A 、各 m ∈ O p m \in \mathrm{Op} m ∈ Op の事前条件 p r e m A ⊆ A × X m \mathrm{pre}^A_m \subseteq A \times X_m pre m A ⊆ A × X m と写像 o p m A : A × X m → A × Y m \mathrm{op}^A_m : A \times X_m \to A \times Y_m op m A : A × X m → A × Y m が与えられているとする。データ型の実装 ( R e p , I n i t , ( o p m ) ) (\mathrm{Rep}, \mathrm{Init}, (\mathrm{op}_m)) ( Rep , Init , ( op m )) 、表現不変条件 I n v \mathrm{Inv} Inv 、抽象化関数 α : I n v → A \alpha : \mathrm{Inv} \to A α : Inv → A が次を満たすとする。
(初期値)I n i t ⊆ I n v \mathrm{Init} \subseteq \mathrm{Inv} Init ⊆ Inv で、すべての r ∈ I n i t r \in \mathrm{Init} r ∈ Init について α ( r ) = a 0 \alpha(r) = a_0 α ( r ) = a 0 。
(可換性)すべての m m m 、r ∈ I n v r \in \mathrm{Inv} r ∈ Inv 、x ∈ X m x \in X_m x ∈ X m について、( α ( r ) , x ) ∈ p r e m A (\alpha(r), x) \in \mathrm{pre}^A_m ( α ( r ) , x ) ∈ pre m A かつ o p m ( r , x ) = ( r ′ , y ) \mathrm{op}_m(r, x) = (r', y) op m ( r , x ) = ( r ′ , y ) ならば、r ′ ∈ I n v r' \in \mathrm{Inv} r ′ ∈ Inv かつ o p m A ( α ( r ) , x ) = ( α ( r ′ ) , y ) \mathrm{op}^A_m(\alpha(r), x) = (\alpha(r'), y) op m A ( α ( r ) , x ) = ( α ( r ′ ) , y ) 。
操作の列 ( m 1 , x 1 ) , … , ( m N , x N ) (m_1, x_1), \ldots, (m_N, x_N) ( m 1 , x 1 ) , … , ( m N , x N ) を、抽象仕様の側で a 0 a_0 a 0 から順に実行した抽象値と結果を a i a_i a i 、y i A y^A_i y i A (o p m i A ( a i − 1 , x i ) = ( a i , y i A ) \mathrm{op}^A_{m_i}(a_{i-1}, x_i) = (a_i, y^A_i) op m i A ( a i − 1 , x i ) = ( a i , y i A ) )とし、各段で事前条件 ( a i − 1 , x i ) ∈ p r e m i A (a_{i-1}, x_i) \in \mathrm{pre}^A_{m_i} ( a i − 1 , x i ) ∈ pre m i A が成り立つとする。同じ列を実装の側で r 0 ∈ I n i t r_0 \in \mathrm{Init} r 0 ∈ Init から順に実行した表現と結果を r i r_i r i 、y i y_i y i (o p m i ( r i − 1 , x i ) = ( r i , y i ) \mathrm{op}_{m_i}(r_{i-1}, x_i) = (r_i, y_i) op m i ( r i − 1 , x i ) = ( r i , y i ) )とする。このとき、すべての i i i について
r i ∈ I n v , α ( r i ) = a i , y i = y i A ( i ≥ 1 ) r_i \in \mathrm{Inv}, \qquad \alpha(r_i) = a_i, \qquad y_i = y^A_i \quad (i \ge 1) r i ∈ Inv , α ( r i ) = a i , y i = y i A ( i ≥ 1 ) が成り立つ。すなわち、呼び出し側が観測する結果の列は、抽象仕様のものと一致する。
証明(命題 6.4) i i i についての帰納法(定理 3.2[証明の技術] )で、「r i ∈ I n v r_i \in \mathrm{Inv} r i ∈ Inv かつ α ( r i ) = a i \alpha(r_i) = a_i α ( r i ) = a i (i ≥ 1 i \ge 1 i ≥ 1 ならさらに y i = y i A y_i = y^A_i y i = y i A )」を示します。
i = 0 i = 0 i = 0 。(初期値)より r 0 ∈ I n v r_0 \in \mathrm{Inv} r 0 ∈ Inv かつ α ( r 0 ) = a 0 \alpha(r_0) = a_0 α ( r 0 ) = a 0 です。
i − 1 i - 1 i − 1 で成り立つとします。r i − 1 ∈ I n v r_{i-1} \in \mathrm{Inv} r i − 1 ∈ Inv 、α ( r i − 1 ) = a i − 1 \alpha(r_{i-1}) = a_{i-1} α ( r i − 1 ) = a i − 1 です。仮定から ( a i − 1 , x i ) ∈ p r e m i A (a_{i-1}, x_i) \in \mathrm{pre}^A_{m_i} ( a i − 1 , x i ) ∈ pre m i A 、すなわち ( α ( r i − 1 ) , x i ) ∈ p r e m i A (\alpha(r_{i-1}), x_i) \in \mathrm{pre}^A_{m_i} ( α ( r i − 1 ) , x i ) ∈ pre m i A です。o p m i ( r i − 1 , x i ) = ( r i , y i ) \mathrm{op}_{m_i}(r_{i-1}, x_i) = (r_i, y_i) op m i ( r i − 1 , x i ) = ( r i , y i ) に(可換性)を使うと、r i ∈ I n v r_i \in \mathrm{Inv} r i ∈ Inv かつ
o p m i A ( a i − 1 , x i ) = o p m i A ( α ( r i − 1 ) , x i ) = ( α ( r i ) , y i ) \mathrm{op}^A_{m_i}(a_{i-1}, x_i) = \mathrm{op}^A_{m_i}(\alpha(r_{i-1}), x_i) = (\alpha(r_i), y_i) op m i A ( a i − 1 , x i ) = op m i A ( α ( r i − 1 ) , x i ) = ( α ( r i ) , y i ) です。左辺は定義により ( a i , y i A ) (a_i, y^A_i) ( a i , y i A ) なので、α ( r i ) = a i \alpha(r_i) = a_i α ( r i ) = a i かつ y i = y i A y_i = y^A_i y i = y i A です。
∎
呼び出し側は表現 r i r_i r i を見られず、結果 y i y_i y i だけを見ます。その y i y_i y i が仕様のものと一致するので、呼び出し側からは実装と仕様を区別できません。これは C. A. R. Hoare が 1972 年に示した、データ表現の正しさの証明法のもっとも簡単な形です。次々章のモデルベーステストは、この命題の対偶(結果の列が食い違えば、可換性がどこかで破れている)を使います。
例 6.5 (整列済みリストによる集合 )
例 6.3 の SortedSet で、抽象値を整数の有限集合、α ( x s ) = { x s [ 0 ] , … , x s [ ℓ − 1 ] } \alpha(xs) = \{xs[0], \ldots, xs[\ell - 1]\} α ( x s ) = { x s [ 0 ] , … , x s [ ℓ − 1 ]} (リストの要素の集合)とします。抽象仕様は a d d A ( S , x ) = ( S ∪ { x } , なし ) \mathrm{add}^A(S, x) = (S \cup \{x\}, \text{なし}) add A ( S , x ) = ( S ∪ { x } , なし ) 、c o n t a i n s A ( S , x ) = ( S , [ x ∈ S ] ) \mathrm{contains}^A(S, x) = (S, [x \in S]) contains A ( S , x ) = ( S , [ x ∈ S ]) です。可換性を確かめます。xs が狭義昇順のとき、bisect_left(xs, x) は下限位置 i i i を返します(定理 3.2[探索アルゴリズム] )。x ∈ α ( x s ) x \in \alpha(xs) x ∈ α ( x s ) なら x s [ i ] = x xs[i] = x x s [ i ] = x で、add は何もせず、α \alpha α は S = S ∪ { x } S = S \cup \{x\} S = S ∪ { x } のままです。x ∉ α ( x s ) x \notin \alpha(xs) x ∈ / α ( x s ) なら i i i の左はすべて x x x より小さく、i i i 以降はすべて x x x より大きいので、位置 i i i への挿入は狭義昇順を保ち、要素の集合は S ∪ { x } S \cup \{x\} S ∪ { x } になります。in は i < ℓ i \lt \ell i < ℓ かつ x s [ i ] = x xs[i] = x x s [ i ] = x を返し、これは x ∈ S x \in S x ∈ S と同値です。命題 6.4 により、SortedSet は表現を漏らさないかぎり集合として振る舞います。
可換性の検証で、不変条件(狭義昇順)を 2 度使いました。二分探索が正しい位置を返すためと、挿入後も昇順であることを言うためです。不変条件のない表現(整列していないリスト)でも集合は実装できますが、そのときは α \alpha α は同じでも操作の実装が線形探索になります。不変条件を強くするほど、操作の実装は多くを仮定でき、速くできる代わりに、保存の義務が重くなります。
例 6.6 (有理数クラスの不変条件 )
有理数を整数の組 ( p , q ) (p, q) ( p , q ) で表し、I n v \mathrm{Inv} Inv を「q > 0 q \gt 0 q > 0 かつ gcd ( p , q ) = 1 \gcd(p, q) = 1 g cd( p , q ) = 1 」、α ( p , q ) = p / q ∈ Q \alpha(p, q) = p/q \in \mathbb{Q} α ( p , q ) = p / q ∈ Q とします。この不変条件のもとで α \alpha α は単射です。p / q = p ′ / q ′ p/q = p'/q' p / q = p ′ / q ′ なら p q ′ = p ′ q pq' = p'q p q ′ = p ′ q で、gcd ( p , q ) = 1 \gcd(p, q) = 1 g cd( p , q ) = 1 より q ∣ q ′ q \mid q' q ∣ q ′ 、対称に q ′ ∣ q q' \mid q q ′ ∣ q 、分母は正なので q = q ′ q = q' q = q ′ 、よって p = p ′ p = p' p = p ′ です。したがって有理数として等しいことと、表現が等しいことが同値 になり、等価性の判定を成分の比較で済ませられます。ハッシュ値を成分から計算してよいのも同じ理由です。
加法を ( p , q ) + ( p ′ , q ′ ) = ( p q ′ + p ′ q , q q ′ ) (p, q) + (p', q') = (pq' + p'q,\ qq') ( p , q ) + ( p ′ , q ′ ) = ( p q ′ + p ′ q , q q ′ ) とだけ実装すると、(保存)が破れます。( 1 , 2 ) + ( 1 , 2 ) = ( 4 , 4 ) (1, 2) + (1, 2) = (4, 4) ( 1 , 2 ) + ( 1 , 2 ) = ( 4 , 4 ) で gcd = 4 \gcd = 4 g cd= 4 です。α \alpha α の値は 1 1 1 で正しいので、足し算の結果を表示するだけのテストは通りますが、( 4 , 4 ) (4, 4) ( 4 , 4 ) と ( 1 , 1 ) (1, 1) ( 1 , 1 ) の成分比較は「等しくない」と答えます。結果を g = gcd ( p q ′ + p ′ q , q q ′ ) g = \gcd(pq' + p'q, qq') g = g cd( p q ′ + p ′ q , q q ′ ) で割って ( ( p q ′ + p ′ q ) / g , q q ′ / g ) ((pq' + p'q)/g,\ qq'/g) (( p q ′ + p ′ q ) / g , q q ′ / g ) を返せば、分母は正のまま、成分は互いに素になり、(保存)が成り立ちます。構築子も同じ正規化をして、q = 0 q = 0 q = 0 を事前条件で排除し、q < 0 q \lt 0 q < 0 なら符号を分子に移す必要があります。
インターフェースを通してモジュールを使う利点は、実装を取り替えられることでした。取り替えてよい条件を、契約の強弱で述べます。
定義 7.1 (契約の精密化(振る舞いの部分型) ) ↗
C , C ′ \mathcal{C}, \mathcal{C}' C , C ′ を契約表(定義 3.7 )とする。すべての ( m , P , Q ) ∈ C (m, P, Q) \in \mathcal{C} ( m , P , Q ) ∈ C に対して、
P ⇒ P ′ かつ Q ′ ⇒ Q P \Rightarrow P' \qquad \text{かつ} \qquad Q' \Rightarrow Q P ⇒ P ′ かつ Q ′ ⇒ Q を満たす ( m , P ′ , Q ′ ) ∈ C ′ (m, P', Q') \in \mathcal{C}' ( m , P ′ , Q ′ ) ∈ C ′ が存在するとき、C ′ \mathcal{C}' C ′ は C \mathcal{C} C を精密化する という。C \mathcal{C} C を上位型の契約、C ′ \mathcal{C}' C ′ を下位型の契約とみるとき、下位型は上位型の振る舞いの部分型 であるという。
下位型は、事前条件を弱めて よく(より多くの状況で呼べる)、事後条件を強めて よい(より多くを約束する)。逆は許されません。
定理 7.2 (置換原則 ) ✓
契約表 C ′ \mathcal{C}' C ′ が C \mathcal{C} C を精密化し、解釈 M ′ \mathcal{M}' M ′ が C ′ \mathcal{C}' C ′ を満たすとする。このとき
M ′ \mathcal{M}' M ′ は C \mathcal{C} C も満たす。
⊢ C { A } c { B } \vdash_{\mathcal{C}} \{A\}\ c\ \{B\} ⊢ C { A } c { B } ならば ⊨ M ′ { A } c { B } \models_{\mathcal{M}'} \{A\}\ c\ \{B\} ⊨ M ′ { A } c { B } である。すなわち、上位型の契約だけを使って導出した呼び出し側の三つ組は、操作を下位型の実装に差し替えても部分正当である。
証明(定理 7.2) (1) ( m , P , Q ) ∈ C (m, P, Q) \in \mathcal{C} ( m , P , Q ) ∈ C をとります。精密化の定義から、P ⇒ P ′ P \Rightarrow P' P ⇒ P ′ かつ Q ′ ⇒ Q Q' \Rightarrow Q Q ′ ⇒ Q を満たす ( m , P ′ , Q ′ ) ∈ C ′ (m, P', Q') \in \mathcal{C}' ( m , P ′ , Q ′ ) ∈ C ′ があり、M ′ \mathcal{M}' M ′ は C ′ \mathcal{C}' C ′ を満たすので ⊨ M ′ { P ′ } c a l l m { Q ′ } \models_{\mathcal{M}'} \{P'\}\ \mathtt{call}\ m\ \{Q'\} ⊨ M ′ { P ′ } call m { Q ′ } です。P ( σ ) P(\sigma) P ( σ ) かつ ⟨ c a l l m , σ ⟩ ⇓ σ ′ \langle \mathtt{call}\ m, \sigma \rangle \Downarrow \sigma' ⟨ call m , σ ⟩ ⇓ σ ′ (M ′ \mathcal{M}' M ′ のもとで)とすると、P ⇒ P ′ P \Rightarrow P' P ⇒ P ′ より P ′ ( σ ) P'(\sigma) P ′ ( σ ) 、したがって Q ′ ( σ ′ ) Q'(\sigma') Q ′ ( σ ′ ) 、Q ′ ⇒ Q Q' \Rightarrow Q Q ′ ⇒ Q より Q ( σ ′ ) Q(\sigma') Q ( σ ′ ) です。よって ⊨ M ′ { P } c a l l m { Q } \models_{\mathcal{M}'} \{P\}\ \mathtt{call}\ m\ \{Q\} ⊨ M ′ { P } call m { Q } で、これがすべての ( m , P , Q ) ∈ C (m, P, Q) \in \mathcal{C} ( m , P , Q ) ∈ C について成り立つので、M ′ \mathcal{M}' M ′ は C \mathcal{C} C を満たします。この議論は帰結の規則 (H-Conseq) の正しさと同じものです。
(2) (1) より M ′ \mathcal{M}' M ′ は C \mathcal{C} C を満たすので、定理 3.8 を契約表 C \mathcal{C} C と解釈 M ′ \mathcal{M}' M ′ に使えば ⊨ M ′ { A } c { B } \models_{\mathcal{M}'} \{A\}\ c\ \{B\} ⊨ M ′ { A } c { B } です。
∎
この定理は、B. Liskov と J. Wing が 1994 年に定式化した置換原則の、契約による形です。定理の内容は「呼び出し側の証明は、操作の実装ではなく契約表 C \mathcal{C} C にしか依存しない」ことに尽きます。健全性定理を、解釈を任意にとって述べておいたのはこのためです。
型との対応。 関数型の部分型は、引数について反変、戻り値について共変でした(命題 6.7[プログラミング言語論] )。型 T T T を「値が T T T に属する」という表明とみれば、関数型 S 1 → S 2 S_1 \to S_2 S 1 → S 2 は契約(事前条件 x ∈ S 1 x \in S_1 x ∈ S 1 、事後条件 結果 ∈ S 2 \in S_2 ∈ S 2 )です。T 1 ≤ S 1 T_1 \le S_1 T 1 ≤ S 1 は「x ∈ T 1 ⇒ x ∈ S 1 x \in T_1 \Rightarrow x \in S_1 x ∈ T 1 ⇒ x ∈ S 1 」、S 2 ≤ T 2 S_2 \le T_2 S 2 ≤ T 2 は「結果 ∈ S 2 ⇒ \in S_2 \Rightarrow ∈ S 2 ⇒ 結果 ∈ T 2 \in T_2 ∈ T 2 」なので、S 1 → S 2 ≤ T 1 → T 2 S_1 \to S_2 \le T_1 \to T_2 S 1 → S 2 ≤ T 1 → T 2 の条件は、定義 7.1 で上位型を T 1 → T 2 T_1 \to T_2 T 1 → T 2 、下位型を S 1 → S 2 S_1 \to S_2 S 1 → S 2 としたものにちょうど一致します。型検査器が確かめられるのは型で書ける契約だけで、「高さは変わらない」のような契約は型では書けないので、型検査を通る下位クラスが振る舞いの部分型であるとは限りません。次の例がそれです。
例 7.3 (正方形は長方形の振る舞いの部分型か )
長方形の操作 set_width(w) の契約を、実行前の高さ h 0 h_0 h 0 ごとに
事前条件 P P P : w > 0 ∧ h e i g h t = h 0 w \gt 0 \wedge \mathit{height} = h_0 w > 0 ∧ height = h 0
事後条件 Q Q Q : w i d t h = w ∧ h e i g h t = h 0 \mathit{width} = w \wedge \mathit{height} = h_0 width = w ∧ height = h 0
とします。「正方形は長方形の一種」なので下位クラスにしたくなりますが、正方形は幅を変えると高さも変わります。
# Python 3.12.14(標準ライブラリのみ)
""" 契約: set_width(w) は w > 0 を事前条件とし、事後に width == w かつ height は変わらない。 """
def __init__ ( self , width , height ) :
self .width, self .height = width, height
return self .width * self .height
""" 事後条件を弱めた下位クラス: 「height は変わらない」を守らない。 """
def __init__ ( self , side ) :
super (). __init__ ( side , side )
self .width = self .height = w
class SmallRectangle ( Rectangle ):
""" 事前条件を強めた下位クラス: w <= 10 を要求する。 """
raise ValueError ( " w は 10 以下 " )
""" Rectangle の契約だけを使って書いた呼び出し側。事前条件 w > 0 は満たしている。 """
assert r. area () == 20 * h, f "面積 { r. area () } != { 20 * h } "
for obj in ( Rectangle ( 3 , 5 ), Square ( 5 ), SmallRectangle ( 3 , 5 )):
print ( type ( obj ) .__name__ , " -> " , client ( obj ))
except ( AssertionError , ValueError ) as e:
print ( type ( obj ) .__name__ , " -> " , type ( e ) .__name__ + " : " , e )
Square -> AssertionError: 面積 400 != 100
SmallRectangle -> ValueError: w は 10 以下
client は、契約 ( P , Q ) (P, Q) ( P , Q ) から「set_width(20) のあとの面積は 20 h 0 20 h_0 20 h 0 」を導いています。これは ⊢ C \vdash_{\mathcal{C}} ⊢ C の導出で、Rectangle では 定理 3.8 のとおり成り立ちます。
Square の set_width の事後条件は Q ′ Q' Q ′ : w i d t h = w ∧ h e i g h t = w \mathit{width} = w \wedge \mathit{height} = w width = w ∧ height = w です。h 0 = 5 h_0 = 5 h 0 = 5 、w = 20 w = 20 w = 20 のとき Q ′ Q' Q ′ を満たす状態は h e i g h t = 20 \mathit{height} = 20 height = 20 で、Q Q Q を満たしません。Q ′ ⇒ Q Q' \Rightarrow Q Q ′ ⇒ Q が成り立たないので 定義 7.1 の条件が破れ、定理 7.2 は適用できません。実際に呼び出し側の表明が落ちています。SmallRectangle は事前条件が P ′ P' P ′ : 0 < w ≤ 10 ∧ h e i g h t = h 0 0 \lt w \le 10 \wedge \mathit{height} = h_0 0 < w ≤ 10 ∧ height = h 0 で、P ⇒ P ′ P \Rightarrow P' P ⇒ P ′ が成り立ちません(w = 20 w = 20 w = 20 )。呼び出し側は上位型の事前条件を守っているのに、例外を受け取ります。
Square を「幅と高さが等しい」という不変条件をもつ不変な値 (set_width を持たない)として設計すれば、問題は起きません。壊れるのは「正方形は長方形である」という包含関係ではなく、「正方形は、長方形の変更操作の契約を守る」という主張です。
契約を assert としてコードに書くと、実行のたびに検査できます。二分探索に、事前条件・ループ不変条件・変量・事後条件をすべて書いてみます。
例 8.1 (表明つきの二分探索と、誤りが見つかる入力 )
buggy=True のとき、hi = mid を hi = mid - 1 と誤ります(mid 自身がまだ答えの候補であることを忘れる、よくある誤りです)。{ 0 , 1 , 2 } \{0, 1, 2\} { 0 , 1 , 2 } 上の長さ 4 4 4 以下の昇順の列すべてと、x ∈ { 0 , 1 , 2 , 3 } x \in \{0, 1, 2, 3\} x ∈ { 0 , 1 , 2 , 3 } の組 140 140 140 個を入力にします。
# Python 3.12.14(標準ライブラリのみ)
from itertools import combinations_with_replacement
def lower_bound ( a , x , buggy= False ) :
""" a[i] >= x となる最小の i を返す(無ければ len(a))。契約を assert で書いたもの。 """
assert all ( a [ i ] <= a [ i + 1 ] for i in range ( n - 1 )), " 事前条件: a は昇順 "
assert 0 <= lo <= hi <= n, " 不変条件 I1 "
assert all ( a [ i ] < x for i in range ( lo )), " 不変条件 I2 "
assert all ( a [ i ] >= x for i in range ( hi , n )), " 不変条件 I3 "
mid = lo + (hi - lo) // 2
hi = mid - 1 if buggy else mid # 誤り: mid 自身を答えの候補から外してしまう
assert 0 <= hi - lo < variant, " 変量 hi - lo が減り、負にならない "
assert 0 <= lo <= hi <= n, " 不変条件 I1 "
assert all ( a [ i ] < x for i in range ( lo )), " 不変条件 I2 "
assert all ( a [ i ] >= x for i in range ( hi , n )), " 不変条件 I3 "
assert lo == sum ( 1 for v in a if v < x ), " 事後条件: 返り値は下限位置 "
total, failed, first = 0 , {}, None
for a in combinations_with_replacement ( range ( 3 ) , n ): # {0,1,2} 上の昇順の列
lower_bound ( list ( a ) , x , buggy )
except AssertionError as e:
failed[ str ( e )] = failed. get ( str ( e ) , 0 ) + 1
first = first or ( list ( a ), x, str ( e ))
return total, failed, first
for buggy in ( False , True ):
total, failed, first = survey ( buggy )
print ( " 誤りあり " if buggy else " 正しい実装 " , " | 入力 " , total , " 個 | 表明違反 " , sum ( failed. values ()) , " 個 " )
for k, v in sorted ( failed. items ()):
正しい実装 | 入力 140 個 | 表明違反 0 個
誤りあり | 入力 140 個 | 表明違反 53 個
変量 hi - lo が減り、負にならない : 38
最初の違反: ([0], 0, '変量 hi - lo が減り、負にならない')
読み取れることが 3 つあります。
誤った実装でも、140 140 140 個のうち 87 87 87 個の入力では、どの表明も落ちません。 たとえば x x x が配列のどの要素よりも大きい入力では、誤った枝を一度も通りません。入力の選び方しだいで、誤りは見えないままです。
表明を外して返り値だけを正解と比べると、返り値が誤るのは 140 140 140 個のうち 15 15 15 個です(表明なしの版を別に実行して数えました。たとえば a = [ 0 , 1 ] a = [0, 1] a = [ 0 , 1 ] 、x = 1 x = 1 x = 1 で、正しい答え 1 1 1 に対して 0 0 0 を返します)。残りの 38 38 38 個は、途中で h i < l o hi \lt lo hi < l o という「あってはならない状態」を通るのに、返り値は偶然正しい入力です。途中の表明は、最終結果に現れない誤りも捕まえます。 次章で、これを「感染したが伝播しなかった」誤りと呼びます。
最初に落ちるのは長さ 1 1 1 の配列です。a = [ 0 ] a = [0] a = [ 0 ] 、x = 0 x = 0 x = 0 で m i d = 0 mid = 0 mi d = 0 、h i = − 1 hi = -1 hi = − 1 となり、変量が負になります。小さい入力を系統的に試すことの効き目が分かります。
命題 8.2 (契約の検査の限界 ) ✓
(実行時検査)仕様 ( D , S ) (D, S) ( D , S ) と実装 f f f (定義 2.1 )に対し、違反入力 の集合を V = { x ∈ D ∣ f ( x ) が定まらないか、 ( x , f ( x ) ) ∉ S } V = \{x \in D \mid f(x) \text{ が定まらないか、} (x, f(x)) \notin S\} V = { x ∈ D ∣ f ( x ) が定まらないか、 ( x , f ( x )) ∈ / S } とする。f f f が仕様を満たすことは V = ∅ V = \emptyset V = ∅ と同値である。有限集合 T ⊆ D T \subseteq D T ⊆ D の各入力で f f f を実行し、止まった結果について ( x , f ( x ) ) ∈ S (x, f(x)) \in S ( x , f ( x )) ∈ S を検査する実行時検査は、T ∩ V = ∅ T \cap V = \emptyset T ∩ V = ∅ のとき違反を 1 つも報告しない。V ≠ ∅ V \ne \emptyset V = ∅ であっても同じである。
(静的な判定)L \mathcal{L} L をチューリング完全なプログラミング言語とする。L \mathcal{L} L のプログラム c c c について、三つ組 { t r u e } c { f a l s e } \{\mathrm{true}\}\ c\ \{\mathrm{false}\} { true } c { false } が部分正当であるかどうか(すなわち、c c c がどの入力でも停止しないかどうか)は決定可能でない。したがって、プログラムと契約 ( P , Q ) (P, Q) ( P , Q ) から { P } c { Q } \{P\}\ c\ \{Q\} { P } c { Q } の部分正当性を判定するアルゴリズムは存在せず、「受理したものは必ず部分正当」である決定可能な検証器は、部分正当なのに受理しない三つ組を必ずもつ。
証明(命題 8.2) (1) f f f が仕様を満たすとは、すべての x ∈ D x \in D x ∈ D で f ( x ) f(x) f ( x ) が定まり ( x , f ( x ) ) ∈ S (x, f(x)) \in S ( x , f ( x )) ∈ S となることで、これは V V V に元が無いことです。実行時検査が違反を報告するのは、ある x ∈ T x \in T x ∈ T で f ( x ) f(x) f ( x ) が定まり ( x , f ( x ) ) ∉ S (x, f(x)) \notin S ( x , f ( x )) ∈ / S のときだけで、そのような x x x は T ∩ V T \cap V T ∩ V に属します。T ∩ V = ∅ T \cap V = \emptyset T ∩ V = ∅ ならば報告はありません。この議論は V V V が空かどうかを使っていません。
(2) 部分正当性の定義(定義 3.3 )から、{ t r u e } c { f a l s e } \{\mathrm{true}\}\ c\ \{\mathrm{false}\} { true } c { false } が部分正当であることは、「どの状態 σ \sigma σ からも、c c c が停止して至る状態 σ ′ \sigma' σ ′ が無い」こと、すなわち c c c がどの状態からも停止しないことと同値です(f a l s e \mathrm{false} false を満たす σ ′ \sigma' σ ′ は無いので)。
停止問題の集合 K K K (定義 7.1[計算可能性] )の元かどうかを判定したい番号 e e e が与えられたとします。L \mathcal{L} L はチューリング完全なので、入力を無視して「機械 M e M_e M e を入力 e e e で模倣し、模倣が止まったら自分も止まる」プログラム c e c_e c e を、e e e から機械的に作れます。これは健全な静的検査の不完全性(定理 5.6[プログラミング言語論] )の証明で使われたのと同じ構成です。c e c_e c e は、e ∈ K e \in K e ∈ K ならどの状態からも停止し、e ∉ K e \notin K e ∈ / K ならどの状態からも停止しません。よって
{ t r u e } c e { f a l s e } が部分正当 ⟺ e ∉ K \{\mathrm{true}\}\ c_e\ \{\mathrm{false}\} \text{ が部分正当} \iff e \notin K { true } c e { false } が部分正当 ⟺ e ∈ / K です。左辺を判定するアルゴリズムがあれば、e e e から c e c_e c e を作ってそれに渡し、答えを反転することで K K K を判定できます。これは K K K が決定可能でないこと(定理 7.2[計算可能性] )に反します。
後半。( P , Q ) = ( t r u e , f a l s e ) (P, Q) = (\mathrm{true}, \mathrm{false}) ( P , Q ) = ( true , false ) の場合がすでに判定できないので、一般の契約についても判定できません。検証器が受理する三つ組の集合 C C C が決定可能で、部分正当な三つ組の集合 V ′ V' V ′ に含まれるとします。C = V ′ C = V' C = V ′ なら V ′ V' V ′ が決定可能になって矛盾するので、C ⊊ V ′ C \subsetneq V' C ⊊ V ′ です。
∎
(1) は当たり前の主張ですが、次章の出発点です。テストで分かるのは T ∩ V T \cap V T ∩ V が空かどうかだけで、知りたいのは V V V が空かどうかです。両者の差を埋める方法は 2 つあります。1 つは、T T T を V V V に当たりやすいように選ぶこと(テストの設計。次章と次々章)。もう 1 つは、V = ∅ V = \emptyset V = ∅ をすべての入力について証明すること(この章の Hoare 論理)。(2) は後者に機械的な近道がないことを言っています。それでも、例 3.10 のように人が不変条件を与えれば、残りの検証条件の多くは機械で確かめられます。決定不能性は「どんなプログラムにも通用する全自動の方法はない」と言っているだけで、目の前の 1 つのプログラムを証明できないとは言っていません。
演習 9.1 易
A , B A, B A , B を整数の定数とする。命令 c c c を x : = x + y ; y : = x − y ; x : = x − y x := x + y;\ y := x - y;\ x := x - y x := x + y ; y := x − y ; x := x − y とするとき、w l p ( c , x = A ∧ y = B ) \mathrm{wlp}(c,\ x = A \wedge y = B) wlp ( c , x = A ∧ y = B ) を 定理 4.2 (5) の計算規則で求めよ。この命令は何をしているか。
解答 後ろから順に代入します。Q 0 Q_0 Q 0 を x = A ∧ y = B x = A \wedge y = B x = A ∧ y = B とします。
x : = x − y x := x - y x := x − y について: Q 1 = Q 0 [ x − y / x ] = ( x − y = A ∧ y = B ) Q_1 = Q_0[x - y/x] = (x - y = A \wedge y = B) Q 1 = Q 0 [ x − y / x ] = ( x − y = A ∧ y = B ) 。
y : = x − y y := x - y y := x − y について: Q 2 = Q 1 [ x − y / y ] = ( x − ( x − y ) = A ∧ x − y = B ) = ( y = A ∧ x − y = B ) Q_2 = Q_1[x - y/y] = (x - (x - y) = A \wedge x - y = B) = (y = A \wedge x - y = B) Q 2 = Q 1 [ x − y / y ] = ( x − ( x − y ) = A ∧ x − y = B ) = ( y = A ∧ x − y = B ) 。
x : = x + y x := x + y x := x + y について: Q 3 = Q 2 [ x + y / x ] = ( y = A ∧ ( x + y ) − y = B ) = ( y = A ∧ x = B ) Q_3 = Q_2[x + y/x] = (y = A \wedge (x + y) - y = B) = (y = A \wedge x = B) Q 3 = Q 2 [ x + y / x ] = ( y = A ∧ ( x + y ) − y = B ) = ( y = A ∧ x = B ) 。
逐次の規則から w l p ( c , Q 0 ) = Q 3 = ( x = B ∧ y = A ) \mathrm{wlp}(c, Q_0) = Q_3 = (x = B \wedge y = A) wlp ( c , Q 0 ) = Q 3 = ( x = B ∧ y = A ) です。実行前に x = B x = B x = B 、y = A y = A y = A なら実行後に x = A x = A x = A 、y = B y = B y = B なので、c c c は補助変数を使わずに x x x と y y y の値を入れ替えています。言語 W の整数は範囲が無限なので正しいのですが、有限の整数型では x + y x + y x + y が範囲を超える場合があり、例 5.4 と同じ注意が要ります。
演習 9.2 標準
X ≥ 0 X \ge 0 X ≥ 0 、Y > 0 Y \gt 0 Y > 0 を整数の定数とする。次のプログラムについて、事後条件 X = q × Y + r ∧ 0 ≤ r < Y X = q \times Y + r \wedge 0 \le r \lt Y X = q × Y + r ∧ 0 ≤ r < Y を導くループ不変条件と、停止性を示すループ変量を与え、条件を確かめよ。
q : = 0 ; r : = X ; w h i l e Y ≤ r d o ( r : = r − Y ; q : = q + 1 ) q := 0;\ r := X;\ \mathtt{while}\ Y \le r\ \mathtt{do}\ (r := r - Y;\ q := q + 1) q := 0 ; r := X ; while Y ≤ r do ( r := r − Y ; q := q + 1 ) 解答 不変条件を I I I : X = q × Y + r ∧ r ≥ 0 X = q \times Y + r \wedge r \ge 0 X = q × Y + r ∧ r ≥ 0 とします。
初期化。 ( I [ X / r ] ) [ 0 / q ] (I[X/r])[0/q] ( I [ X / r ]) [ 0/ q ] は X = 0 × Y + X ∧ X ≥ 0 X = 0 \times Y + X \wedge X \ge 0 X = 0 × Y + X ∧ X ≥ 0 で、X ≥ 0 X \ge 0 X ≥ 0 から成り立ちます。
保存。 w l p ( r : = r − Y ; q : = q + 1 , I ) \mathrm{wlp}(r := r - Y;\ q := q + 1,\ I) wlp ( r := r − Y ; q := q + 1 , I ) は、まず I [ q + 1 / q ] = ( X = ( q + 1 ) Y + r ∧ r ≥ 0 ) I[q + 1/q] = (X = (q + 1) Y + r \wedge r \ge 0) I [ q + 1/ q ] = ( X = ( q + 1 ) Y + r ∧ r ≥ 0 ) 、次に r r r を r − Y r - Y r − Y に置き換えて X = ( q + 1 ) Y + ( r − Y ) ∧ r − Y ≥ 0 X = (q + 1) Y + (r - Y) \wedge r - Y \ge 0 X = ( q + 1 ) Y + ( r − Y ) ∧ r − Y ≥ 0 、すなわち X = q Y + r ∧ r ≥ Y X = qY + r \wedge r \ge Y X = q Y + r ∧ r ≥ Y です。これは I ∧ Y ≤ r I \wedge Y \le r I ∧ Y ≤ r から従います。
終了時。 I ∧ ¬ ( Y ≤ r ) I \wedge \lnot(Y \le r) I ∧ ¬ ( Y ≤ r ) は X = q Y + r ∧ 0 ≤ r < Y X = qY + r \wedge 0 \le r \lt Y X = q Y + r ∧ 0 ≤ r < Y で、求める事後条件そのものです。定理 3.8 により部分正当です。
変量。 v = r v = r v = r とします。I ∧ Y ≤ r I \wedge Y \le r I ∧ Y ≤ r のもとで r ≥ Y > 0 r \ge Y \gt 0 r ≥ Y > 0 なので v ≥ 0 v \ge 0 v ≥ 0 。本体は代入 2 つなので停止し、I I I を保ち、r r r は Y ≥ 1 Y \ge 1 Y ≥ 1 だけ減るので v v v は真に減ります。定理 5.2 より、プログラムは全正当です。Y > 0 Y \gt 0 Y > 0 を仮定から外すと、Y = 0 Y = 0 Y = 0 のとき v v v が減らず、実際にループは止まりません。
演習 9.3 標準
w = w h i l e b d o c w = \mathtt{while}\ b\ \mathtt{do}\ c w = while b do c とする。任意の表明 Q Q Q について
w l p ( w , Q ) = ( b ∧ w l p ( c , w l p ( w , Q ) ) ) ∨ ( ¬ b ∧ Q ) \mathrm{wlp}(w, Q) = \bigl(b \wedge \mathrm{wlp}(c, \mathrm{wlp}(w, Q))\bigr) \vee \bigl(\lnot b \wedge Q\bigr) wlp ( w , Q ) = ( b ∧ wlp ( c , wlp ( w , Q )) ) ∨ ( ¬ b ∧ Q ) が成り立つことを示せ。
解答 σ \sigma σ を状態とします。
⟦ b ⟧ σ = f f \llbracket b \rrbracket\sigma = \mathrm{ff} [ [ b ] ] σ = ff のとき。w w w の実行の導出は (WhF) でしか終われないので、⟨ w , σ ⟩ ⇓ σ ′ \langle w, \sigma \rangle \Downarrow \sigma' ⟨ w , σ ⟩ ⇓ σ ′ は σ ′ = σ \sigma' = \sigma σ ′ = σ と同値です。よって σ ∈ w l p ( w , Q ) \sigma \in \mathrm{wlp}(w, Q) σ ∈ wlp ( w , Q ) は Q ( σ ) Q(\sigma) Q ( σ ) と同値で、右辺に属すること(b b b の側は成り立たないので Q ( σ ) Q(\sigma) Q ( σ ) )と一致します。
⟦ b ⟧ σ = t t \llbracket b \rrbracket\sigma = \mathrm{tt} [ [ b ] ] σ = tt のとき。w w w の実行の導出は (WhT) でしか終われないので、⟨ w , σ ⟩ ⇓ σ ′ \langle w, \sigma \rangle \Downarrow \sigma' ⟨ w , σ ⟩ ⇓ σ ′ は「⟨ c , σ ⟩ ⇓ σ 1 \langle c, \sigma \rangle \Downarrow \sigma_1 ⟨ c , σ ⟩ ⇓ σ 1 かつ ⟨ w , σ 1 ⟩ ⇓ σ ′ \langle w, \sigma_1 \rangle \Downarrow \sigma' ⟨ w , σ 1 ⟩ ⇓ σ ′ となる σ 1 \sigma_1 σ 1 がある」ことと同値です。これは ⟨ c ; w , σ ⟩ ⇓ σ ′ \langle c; w, \sigma \rangle \Downarrow \sigma' ⟨ c ; w , σ ⟩ ⇓ σ ′ と同じ条件なので、σ ∈ w l p ( w , Q ) \sigma \in \mathrm{wlp}(w, Q) σ ∈ wlp ( w , Q ) は σ ∈ w l p ( c ; w , Q ) \sigma \in \mathrm{wlp}(c; w, Q) σ ∈ wlp ( c ; w , Q ) と同値で、定理 4.2 (5) の逐次の規則により σ ∈ w l p ( c , w l p ( w , Q ) ) \sigma \in \mathrm{wlp}(c, \mathrm{wlp}(w, Q)) σ ∈ wlp ( c , wlp ( w , Q )) と同値です。右辺に属することもこれと一致します。
この等式は w l p ( w , Q ) \mathrm{wlp}(w, Q) wlp ( w , Q ) を自分自身で表しているので、計算規則にはなりません。ループ不変条件 I I I は、この方程式の解の代わりに人が与える「I ∧ b ⇒ w l p ( c , I ) I \wedge b \Rightarrow \mathrm{wlp}(c, I) I ∧ b ⇒ wlp ( c , I ) かつ I ∧ ¬ b ⇒ Q I \wedge \lnot b \Rightarrow Q I ∧ ¬ b ⇒ Q 」を満たす表明です。
演習 9.4 標準
口座の操作 withdraw(n) の上位型の契約を、実行前の残高 b 0 b_0 b 0 ごとに「事前条件 0 < n ≤ b 0 ∧ b a l a n c e = b 0 0 \lt n \le b_0 \wedge \mathit{balance} = b_0 0 < n ≤ b 0 ∧ balance = b 0 、事後条件 b a l a n c e = b 0 − n \mathit{balance} = b_0 - n balance = b 0 − n 」とする。次の 3 つの下位型のうち、振る舞いの部分型(定義 7.1 )であるものはどれか。そうでないものについては、上位型の契約を守っているのに困る呼び出し側を挙げよ。
(a) 事前条件 0 < n ≤ b 0 + 100 ∧ b a l a n c e = b 0 0 \lt n \le b_0 + 100 \wedge \mathit{balance} = b_0 0 < n ≤ b 0 + 100 ∧ balance = b 0 、事後条件 b a l a n c e = b 0 − n \mathit{balance} = b_0 - n balance = b 0 − n (100 100 100 まで貸し越せる)。
(b) 事前条件 0 < n ≤ b 0 ∧ n ≤ 1000 ∧ b a l a n c e = b 0 0 \lt n \le b_0 \wedge n \le 1000 \wedge \mathit{balance} = b_0 0 < n ≤ b 0 ∧ n ≤ 1000 ∧ balance = b 0 、事後条件 b a l a n c e = b 0 − n \mathit{balance} = b_0 - n balance = b 0 − n (1 回の上限つき)。
(c) 事前条件は上位型と同じ、事後条件 b a l a n c e = b 0 − n − 1 \mathit{balance} = b_0 - n - 1 balance = b 0 − n − 1 (手数料 1 1 1 を引く)。
解答 (a) は振る舞いの部分型です。0 < n ≤ b 0 0 \lt n \le b_0 0 < n ≤ b 0 ならば 0 < n ≤ b 0 + 100 0 \lt n \le b_0 + 100 0 < n ≤ b 0 + 100 なので P ⇒ P ′ P \Rightarrow P' P ⇒ P ′ 、事後条件は同じなので Q ′ ⇒ Q Q' \Rightarrow Q Q ′ ⇒ Q です。定理 7.2 により、上位型の契約で証明された呼び出し側はそのまま正しい。上位型の事前条件を守る呼び出し側は貸し越しを起こさないので、追加された動作に出会いません。
(b) は部分型ではありません。b 0 = 5000 b_0 = 5000 b 0 = 5000 、n = 2000 n = 2000 n = 2000 は P P P を満たし P ′ P' P ′ を満たさないので、P ⇒ P ′ P \Rightarrow P' P ⇒ P ′ が破れます。残高の範囲内で 2000 2000 2000 を引き出す呼び出し側は、上位型の契約を守っているのに、下位型では事前条件違反になります。
(c) も部分型ではありません。Q ′ Q' Q ′ を満たす状態は b a l a n c e = b 0 − n − 1 \mathit{balance} = b_0 - n - 1 balance = b 0 − n − 1 で、Q Q Q (b a l a n c e = b 0 − n \mathit{balance} = b_0 - n balance = b 0 − n )を満たさないので、Q ′ ⇒ Q Q' \Rightarrow Q Q ′ ⇒ Q が破れます。「残高をちょうど 0 0 0 にするために withdraw(balance) を呼ぶ」呼び出し側は、実行後の残高が − 1 -1 − 1 になります。そもそも n = b 0 n = b_0 n = b 0 は事前条件を満たすので、(c) は残高が負にならないという不変条件があればそれも破ります。
C. A. R. Hoare, “An axiomatic basis for computer programming”, Communications of the ACM 12 (1969), 576–580. doi:10.1145/363235.363259 — 三つ組と推論規則の原論文。
E. W. Dijkstra, A Discipline of Programming , Prentice-Hall, 1976 — 最弱事前条件によるプログラムの意味づけと、不変条件・変量からのプログラムの導出。
G. Winskel, The Formal Semantics of Programming Languages: An Introduction , MIT Press, 1993 — 第 2 章(操作的意味論)、第 6 章(Hoare 規則と健全性)、第 7 章(相対完全性)。
B. Meyer, Object-Oriented Software Construction , 2nd ed., Prentice Hall, 1997 — 第 11 章(契約による設計)。
B. H. Liskov, J. M. Wing, “A behavioral notion of subtyping”, ACM Transactions on Programming Languages and Systems 16 (1994), 1811–1841. doi:10.1145/197320.197383 — 振る舞いの部分型の定式化。
C. A. R. Hoare, “Proof of correctness of data representations”, Acta Informatica 1 (1972), 271–281 — 表現不変条件と抽象化関数。
S. A. Cook, “Soundness and completeness of an axiom system for program verification”, SIAM Journal on Computing 7 (1978), 70–90 — 相対完全性。
執筆: Claude Code(Opus 5.5)、Claude Code(Sonnet 5.5)・監査 2 回(Claude Code)・最終 2026-10-12