h5i

第 10 章

タクティク: 対話的に証明項を組み立てる

Lean のユーザーが、第 9 章の末尾にあるような項を手で書くことはめったにない。書くのはタクティクスクリプトであり、残っている証明すべきものをシステムが表示しながら、項を一歩ずつ組み立てていくコマンドの列である。本章では、よく使われるタクティクが本書の体系でどのように項を構築するか、そしてなぜそのどれも信頼する必要がないかを示す。

Lean では、第 9 章の定理は次のように証明される。

theorem zero_add (n : Nat) : 0 + n = n := by
  induction n with
  | zero => rfl
  | succ k ih => rw [Nat.add_succ, ih]

各タクティクは、証明項の一部を組み立てる小さなプログラムであり、カーネルの外側で動く。その出力はふつうの項であり、カーネルは他の項と同じようにアルゴリズム 5.1 でそれを検査する。

ゴールと証明状態

ゴール(goal)⟨Γ⊢?m:T⟩\goal{\Gamma}{\hole{m}}{T} は、文脈 Γ\Gamma における型 TT の項を求める。ホール(hole)?m\hole{m} はまだ見つかっていない項を表し、Lean ではこれをメタ変数と呼ぶ。証明状態(proof state)は、ゴールのリストと、ホールを含みうる項である部分的な証明項を合わせたものである。

TT を証明するには、部分項 ?0\hole{0} と唯一のゴール ⟨⋅⊢?0:T⟩\goal{\cdot}{\hole{0}}{T} から始める。タクティクは最初のゴール ⟨Γ⊢?m:T⟩\goal{\Gamma}{\hole{m}}{T} に対して働き、?m\hole{m} に、新鮮なホールを含みうる項を割り当て、そのゴールを新鮮なホールごとに一つの新しいゴールで置き換える。ゴールが残っていなければ部分項にホールはなく、カーネルがそれを TT に対して検査する。ホールは束縛子の下に置かれることがあり、λ(x:A). ?1\tLam{x}{A}{\hole{1}} の ?1\hole{1} がそうで、それを埋める項はその束縛変数を使ってよい。ゴールがそれぞれ自分の文脈を持つのはそのためである。

各タクティクは、新鮮な各ホールがのちにそのゴールの述べる型の項で埋められると仮定したうえで、自分の割り当てが Γ\Gamma において型 TT を持つことを保証しなければならない。これを ⟨Γ⊢?m:T⟩⇝new goals\goal{\Gamma}{\hole{m}}{T} \leadsto \text{new goals} と書き、続けて割り当てを書く。

最初の例

スクリプト examples/identity.leanmini は第 1 章の冒頭の例を証明し、下のエクスプローラはそれを一コマンドずつ実行する。

goal forall (P : Sort 0), P -> P
intro P
intro p
exact p
qed

ボタンか矢印キーで一歩ずつ進めてほしい。中央のパネルは各コマンドの後のゴールのリスト、右のパネルは部分的な証明項で、現在のゴールに対応するホールが強調されている。三つのパネルは常に一致する。タクティクがホールに項を割り当てるからこそ、ゴールが変わる。

ゴールのリストだけでは足りない。intro p のあと、ゴール PP は、その解が二つの λ\lambda で包まれなければならないことを覚えておらず、部分項がそれを記録する。

基本的なタクティク

intro x。 TT が Π\Pi 型に正規化されるとき、このタクティクはその引数を文脈に移す。

⟨Γ⊢?m:Π(y:A). B⟩  ⇝  ⟨Γ,x:A⊢?1:B[y↦x]⟩,?m:=λ(x:A). ?1.\goal{\Gamma}{\hole{m}}{\tPi{y}{A}{B}} \;\leadsto\; \goal{\Gamma, x : A}{\hole{1}}{\subst{B}{y}{x}}, \qquad \hole{m} \mathrel{:=} \tLam{x}{A}{\hole{1}}.

割り当てが正しい型を持つことは T-Lam による。名前 xx は dom(Γ)\dom(\Gamma) に含まれていてはならず、さもないと新しい仮定が既存の仮定をシャドーイングし、カーネルが不健全になる(第 11 章)。本書の intro はそのような名前を「binder name already in context」というメッセージで拒否するが、Lean は名前を付け替える。

exact t。 このタクティクは、与えられた項でゴールを閉じる。

⟨Γ⊢?m:T⟩  ⇝  no goals,?m:=t,\goal{\Gamma}{\hole{m}}{T} \;\leadsto\; \text{no goals}, \qquad \hole{m} \mathrel{:=} t,

その前に自分で Check(Γ,t,T)\Check(\Gamma, t, T) を実行する。型の正しくない tt はどのみち最後にカーネルが拒否するので、早めに検査するのは、よりよいエラーメッセージのためでしかない。

apply f。 ff が型 A1→⋯→An→BA_1 \to \cdots \to A_n \to B を持ち B≡TB \conv T であるとき、このタクティクは引数を新しいゴールとして残す。

⟨Γ⊢?m:T⟩  ⇝  ⟨Γ⊢?1:A1⟩,…,⟨Γ⊢?n:An⟩,?m:=f  ?1⋯?n.\goal{\Gamma}{\hole{m}}{T} \;\leadsto\; \goal{\Gamma}{\hole{1}}{A_1}, \ldots, \goal{\Gamma}{\hole{n}}{A_n}, \qquad \hole{m} \mathrel{:=} f\;\hole{1} \cdots \hole{n}.

割り当ては T-App を nn 回使って型 BB を持ち、T-Conv により型 TT を持つ。ff の型が依存 Π\Pi のときは、後ろの AiA_i と BB が前の引数に言及するので、タクティクは BB を TT と照合してそれらの引数を見つけなければならない。Lean はこれを単一化で行い、メタ変数について解くが、本書の制限版は非依存の矢印しか扱わず、それ以外は「dependent apply requires elaboration/unification」と報告する。(n=0n = 0 のとき、つまり ff の型がすでに TT と変換可能なら、apply f は exact f と同じに振る舞う。)

たとえば、第 6 章の二つ目の演習だった含意の推移性は、結論から逆向きに進められる。

goal forall (P : Sort 0), forall (Q : Sort 0), forall (R : Sort 0), (P -> Q) -> (Q -> R) -> P -> R
intro P
intro Q
intro R
intro f
intro g
intro p
apply g
apply f
exact p
qed

六回の intro のあと、ゴールは ⟨Γ⊢?6:R⟩\goal{\Gamma}{\hole{6}}{R} で、Γ=P,Q,R:Sort 0,  f:P→Q,  g:Q→R,  p:P\Gamma = P, Q, R : \Sort{0},\; f : P \to Q,\; g : Q \to R,\; p : P である。型 RR の仮定はないので exact では閉じられないが、gg は RR で終わる。apply g は ?6:=g  ?7\hole{6} \mathrel{:=} g\;\hole{7} を割り当て、?7:Q\hole{7} : Q を残す。同様に apply f は ?7:=f  ?8\hole{7} \mathrel{:=} f\;\hole{8} を割り当てて ?8:P\hole{8} : P を残し、これを exact p が閉じる。得られる項の本体は g  (f  p)g\;(f\;p) である。二つのタクティクの違いに注意してほしい。gg の型は Q→RQ \to R であって RR ではないので exact g は失敗する一方、apply g は、gg に足りない引数を新しいゴールに変える。

change T'。 このタクティクは、ゴールを定義的に等しいものに置き換える。

⟨Γ⊢?m:T⟩  ⇝  ⟨Γ⊢?1:T′⟩,?m:=?1,\goal{\Gamma}{\hole{m}}{T} \;\leadsto\; \goal{\Gamma}{\hole{1}}{T'}, \qquad \hole{m} \mathrel{:=} \hole{1},

ただし T′T' は型で、T′≡TT' \conv T でなければならない。項には何も加えず、ゴールの表示の仕方を変えるだけであり、それを正当化するのは T-Conv である。change が必要なのは、rw(後述)が部分項を α 同値までしか探さないからであり、計算してはじめて現れる部分項は、まず change で露出させなければならない。本章末尾の例がそうである。

rfl。 TT が Equ  A  a  b\Eq{u}\;A\;a\;b に正規化され a≡ba \conv b であるとき、このタクティクはゴールを閉じる。

⟨Γ⊢?m:Equ  A  a  b⟩  ⇝  no goals,?m:=reflu  A  a.\goal{\Gamma}{\hole{m}}{\Eq{u}\;A\;a\;b} \;\leadsto\; \text{no goals}, \qquad \hole{m} \mathrel{:=} \refl{u}\;A\;a.

割り当ての型は Equ  A  a  a\Eq{u}\;A\;a\;a で、これはゴールと変換可能なので、rfl が証明するのは、定義的等価で成り立つ等式そのものである(第 9 章)。

帰納法

n:Natn : \Nat を Γ\Gamma の変数とし、Γ⊢T:Sort u\Gamma \vdash T : \Sort{u} とする。タクティク induction n は、ゴールの中の nn を抽象化して motive を作り、構成子ごとに一つのゴールを生成する。

⟨Γ⊢?m:T⟩  ⇝  ⟨Γ⊢?z:T[n↦zero]⟩,  ⟨Γ,k:Nat,ih:T[n↦k]⊢?s:T[n↦succ  k]⟩,?m:=Nat.recu  (λ(z:Nat). T[n↦z])  ?z  (λ(k:Nat). λ(ih:T[n↦k]). ?s)  n.\begin{array}{l} \goal{\Gamma}{\hole{m}}{T} \;\leadsto\; \goal{\Gamma}{\hole{z}}{\subst{T}{n}{\zero}},\; \goal{\Gamma, k : \Nat, \mathit{ih} : \subst{T}{n}{k}}{\hole{s}}{\subst{T}{n}{\suc\;k}}, \\[4pt] \hole{m} \mathrel{:=} \NatRec{u}\;(\tLam{z}{\Nat}{\subst{T}{n}{z}})\;\hole{z}\;(\tLam{k}{\Nat}{\tLam{\mathit{ih}}{\subst{T}{n}{k}}{\hole{s}}})\;n. \end{array}

motive を M=λ(z:Nat). T[n↦z]M = \tLam{z}{\Nat}{\subst{T}{n}{z}} とすると、Nat.recu  M\NatRec{u}\;M の型は M  zeroM\;\zero と Π(k:Nat). M  k→M  (succ  k)\tPi{k}{\Nat}{M\;k \to M\;(\suc\;k)} を求め、結果の型は M  nM\;n であり、これらは β により、二つのゴールの型および TT と変換可能である。この構成が正しいのは、名前が新鮮で、レベル uu が正しく、他のどの仮定も nn に言及しないときに限る。

  • motive が nn ではなく新鮮な名前 zz を束縛するのは、nn がすでに Γ\Gamma にあり、λ(n:Nat). T\tLam{n}{\Nat}{T} と書くとそれをシャドーイングしてしまい、第 11 章の落とし穴にはまるからである。同様に kk と ih\mathit{ih} も新鮮でなければならない。(本書の実装では、motive の変数を ind_x、n が使われているときの後者の構成子のフィールドを k、仮定を ih と名付ける。)
  • motive のレベル uu はゴールの型から来るもので、InferSort\InferSort で求める。命題なら 00 であり、Nat.rec0\NatRec{0} が帰納法である。
  • 他の仮定が nn に言及しているとき、たとえば h:n=2h : n = 2 があるとき、新しいゴールには元の nn についての hh が残ったままで、ih\mathit{ih} はそれについて何も言わず、ゴールは正しいが証明できないかもしれない。Lean の induction はそのような仮定をまずゴールの中に戻し(リバートし)、motive の一部にする一方、本書の実装では、後続の仮定が nn に言及しないことを要求し、そうでなければ「induction: revert dependent hypotheses first」と報告する。

この実装は Nat\Nat に固有のものではなく、添字を持たない任意の帰納型で動く。シグネチャから再帰子の型を読み、構成子ごとに小前提を一つずつ剥がし、それぞれを、構成子のフィールドと帰納法の仮定を文脈に持つゴールに変える。スクリプト examples/generic-list.leanmini は、ユーザーが宣言したリスト型に対してこれを使う。

書き換え

h:Equ  A  a  bh : \Eq{u}\;A\;a\;b とし、ゴール TT を命題とする。タクティク rw h は、ゴールの中の aa を bb に三段階で置き換える。

  1. 抽象化。 新鮮な xx を選び、TT の中の aa のすべての出現を xx で置き換えたものを CC とする。出現は変換ではなく α 同値までで探す。aa の自由変数を束縛する束縛子の下にある出現は、そこでは別のものを指すので飛ばす。
  2. motive の検査。 Γ,x:A⊢C:Sort 0\Gamma, x : A \vdash C : \Sort{0} を検査する。これは後述のとおり失敗しうる。
  3. 項の構築。 新しいゴールは C[x↦b]\subst{C}{x}{b}、つまり aa を bb に置き換えたゴールである。
    ⟨Γ⊢?m:T⟩  ⇝  ⟨Γ⊢?1:C[x↦b]⟩,?m:=Eq.ndrecu,0  A  a  (λ(x:A). C→T)  (λ(p:T). p)  b  h  ?1.\goal{\Gamma}{\hole{m}}{T} \;\leadsto\; \goal{\Gamma}{\hole{1}}{\subst{C}{x}{b}}, \qquad \hole{m} \mathrel{:=} \EqRec{u,0}\;A\;a\;(\tLam{x}{A}{C \to T})\;(\tLam{p}{T}{p})\;b\;h\;\hole{1}.

この割り当てが型 TT を持つことを見るために、motive を N=λ(x:A). C→TN = \tLam{x}{A}{C \to T} と書く。C[x↦a]=T\subst{C}{x}{a} = T なので、項 N  aN\;a は T→TT \to T と変換可能で、恒等関数 λ(p:T). p\tLam{p}{T}{p} がその型を持つ。hh に沿って輸送すると N  bN\;b が得られ、これは C[x↦b]→T\subst{C}{x}{b} \to T と変換可能であり、それを ?1\hole{1} に適用すれば TT になる。Lean は同じ発想から Eq.mpr (congrArg (fun x => C) h) という別の項を組み立て、さらに新しいゴールを rfl で閉じることも試みるが、本書の実装ではそれを明示的な rfl に任せ、二つの段階がスクリプト上で見えるようにしている。

motive の型が正しくないとき。 aa を抽象化するとゴールが壊れることがある。Γ=n:Nat,  h:n=2,  q:n=0\Gamma = n : \Nat,\; h : n = 2,\; q : n = 0 とし、証明 qq についての主張であるゴール Eq0  (n=0)  q  q\Eq{0}\;(n = 0)\;q\;q に rw h を実行したとしよう。nn を抽象化すると C=Eq0  (x=0)  q  qC = \Eq{0}\;(x = 0)\;q\;q となるが、qq の型は n=0n = 0 であって x=0x = 0 ではなく、qq 自身は抽象化できないので、これは型が正しくない。段階 2 が失敗し、Lean も同じ状況を「motive is not type correct」と報告する。段階 2 がなければ、タクティクはカーネルに拒否される項を生成し、それは健全性には無害だが、ユーザーの役には立たない。

例: タクティクによる 0 + n = n

本書の体系では、第 9 章の証明は次のスクリプトになり、これは examples/zero-add.leanmini に add\add の定義を含めたものである(スクリプトでは 0+n0 + n を add Nat.zero n と書く)。

def add : Nat -> Nat -> Nat := fun (m : Nat) => fun (n : Nat) => Nat.rec1 (fun (_n : Nat) => Nat) m (fun (k : Nat) => fun (r : Nat) => Nat.succ r) n
goal forall (n : Nat), Eq Nat (add Nat.zero n) n
intro n
induction n
rfl
change Eq Nat (Nat.succ (add Nat.zero k)) (Nat.succ k)
rw ih
rfl
qed

部分項が育っていく様子を見てほしい。induction n が二つのホールを持つ Nat.rec0\NatRec{0} を挿入し、rfl が最初のホールを埋め、change は項には触れずゴールだけを書き換え、rw ih が輸送を挿入して最後の rfl のためのホールを一つ残す。change の行を消してスクリプトを適用すると、rw が左辺を見つけられずに失敗するのが分かる。

# 第二引数に関する再帰による加法:
#   add m zero     = m
#   add m (succ k) = succ (add m k)
def add : Nat -> Nat -> Nat := fun (m : Nat) => fun (n : Nat) => Nat.rec1 (fun (_n : Nat) => Nat) m (fun (k : Nat) => fun (r : Nat) => Nat.succ r) n

# add zero n = n は定義的ではないので、n に関する帰納法が要る。
goal forall (n : Nat), Eq Nat (add Nat.zero n) n
intro n
induction n
# 基底ケース: add zero zero は zero に簡約される。
rfl
# ステップケース: add zero (succ k) を succ (add zero k) に展開し、ih を使う。
change Eq Nat (Nat.succ (add Nat.zero k)) (Nat.succ k)
rw ih
rfl
qed

この項は、第 9 章で手書きしたものと二箇所で異なる。intro のために λ(n:Nat). …  n\tLam{n}{\Nat}{\ldots\;n} で始まることと、ステップが rw の組み立てた motive を使うことである。どちらも同じ命題の証明であり、カーネルはどちらを受け取っても構わない。プレイグラウンドで change の行を消し、rw が出すエラーを読んでみてほしい。

なぜタクティクを信頼する必要がないのか

カーネルが最終的な項を検査するので、体系が健全であるためにタクティクが正しい必要はない。バグのあるタクティクは、失敗するか、型の正しくない項を生成してカーネルに拒否されるかのどちらかであり、偽の定理をカーネルに受理させることはできない。exact の Check\Check や rw の motive の検査など、タクティク自身が行う検査は、エラーを早く分かりやすく報告するためにあるにすぎない。

したがって体系の信頼すべき部分は小さく、アルゴリズム 5.1、第 III 部の簡約関数と比較関数、そして第 8 章のシグネチャ構築手続きであり、src/proof.rs と src/syntax.rs の中身はすべてその外側にある。

Lean のタクティク基盤は、ここで素描したものよりはるかに豊かで、単一化で解かれるメタ変数、暗黙の引数を補い 0 + n のような記法を展開するエラボレータ、依存する仮定をリバートし添字付きの族も扱う induction、そして simp のような自動化がある。これらはすべて信頼されない側にあり、Lean のカーネルも本書のものと同じく、完成した項しか見ない [de Moura and Ullrich 2021]。

演習

Π(n:Nat). n=n\tPi{n}{\Nat}{n = n} をタクティクで、帰納法を使って証明し、なぜ両方のケースが rfl で閉じるのかを説明せよ。

解答

これは examples/nat-reflexivity.leanmini である。intro n、induction n、rfl、rfl、qed。基底のゴールは 0=00 = 0、ステップのゴールは succ  k=succ  k\suc\;k = \suc\;k で、どちらも両辺が構文的に同一なので、定義的等価は自明に成り立つ。ここで帰納法は不要だが(intro n のあと rfl 一つで済む)、ゴールの形を見るには役に立つ。

ゴールが Eq  Nat  a  c\Eq{}\;\Nat\;a\;c、仮定が h:a=bh : a = b、文脈が a,b,c:Nata, b, c : \Nat のとき、rw が生成する項を書け。

解答

aa を抽象化すると C=(x=c)C = (x = c) となり、新しいゴールは b=cb = c である。割り当ては Eq.ndrec1,0  Nat  a  (λ(x:Nat). (x=c)→(a=c))  (λ(p:a=c). p)  b  h  ?1\EqRec{1,0}\;\Nat\;a\;(\tLam{x}{\Nat}{(x = c) \to (a = c)})\;(\tLam{p}{a = c}{p})\;b\;h\;\hole{1} で、?1:b=c\hole{1} : b = c である。

覚えておくこと

  • 証明状態は、ゴールのリストと、ホールを持つ部分項であり、タクティクは最初のホールに項を割り当て、新鮮なホールごとに新しいゴールを一つ開く。
  • intro は λ\lambda を組み立て、exact は項を差し込み、apply は引数の位置にホールを持つ適用を組み立て、change は T-Conv を使い、rfl は refl\refl{} を使う。
  • induction は変数を motive へ抽象化して再帰子を適用し、rw は左辺を motive へ抽象化して輸送を適用する。
  • カーネルは完成した項を検査し直すので、タクティクは信頼基盤の外側にある。