本章では、いくつかの小さな項に対してアルゴリズム 5.1 を手で実行し、各実行を型付け規則に対応づける。例は、アルゴリズムの仕事を一つずつ見せるように選んであり、導出を組み立てること、誤った証明を捕まえること、仮定を使うこと、全称命題を具体化すること、型の内部で計算することである。以下のトレースはどれも、プレイグラウンドでカーネル上に再現できる。
定理 T の証明 t を検証するために、カーネルは Check(⋅,t,T) を呼び、これが Infer(⋅,t) を呼んでその結果を T と比較する。トレースには、Infer が行う再帰呼び出しと、各呼び出しが対応する型付け規則を示す。ノードを開いたり閉じたりして、再帰を追ってほしい。
恒等証明
Γ1=P:Sort0、Γ2=Γ1,h:P とおく。第 1 章 の冒頭の例の導出は、整形式性の前提を省くと次のようになる。
⋅⊢λ(P:Sort0).λ(h:P).h:Π(P:Sort0).P→P⋅⊢Sort0:Sort1;(T-Sort)Γ1⊢λ(h:P).h:P→PΓ1⊢P:Sort0;(T-Var)Γ2⊢h:P;(T-Var);(T-Lam);(T-Lam)
アルゴリズムは、同じ導出を下から上へ組み立てる。呼び出し Infer(⋅,λ(P:Sort0).λ(h:P).h) は次のように進む。
- Infer(⋅,λ(P:Sort0).λ(h:P).h) T-Lam
- InferSort(⋅,Sort0): Infer が Sort1 を返すので、注釈は型である T-Sort
- 文脈が空なので、Freshen は P をそのままにする
- Infer(Γ1,λ(h:P).h) T-Lam
- InferSort(Γ1,P): Infer が P を引いて Sort0 を返す T-Var
- Freshen は h をそのままにする
- Infer(Γ2,h) は h を引いて P を返す T-Var
- Π(h:P).P を返す
- Π(P:Sort0).Π(h:P).P を返す
型を返す各呼び出しは、導出木の一つのノードに対応し、計算を使う呼び出しは一つもない。最後に Check が、結果を宣言された型 Π(P:Sort0).P→P と比較する。P→P は Π(_:P).P の略記であり、h も _ も本体には現れないので、両者の違いは束縛変数一つの名前だけである。したがって IsDefEq は成功し、証明は受理される。
同じ実行を、カーネル自身に記録させたものが次であり、呼び出しビューは上のリストと、証明木ビューは導出と一致する。
# 同じ証明を、完全な項として与える。カーネルはその型を推論し、
# ゴールと比較する。
goal forall (P : Sort 0), P -> P
exact fun (P : Sort 0) => fun (h : P) => h
qed
誤った証明
利用者が、t=λ(P:Sort0).λ(h:P).P は Π(P:Sort0).P→P を証明すると主張したとしよう。本体は証明 h ではなく命題 P を返している。カーネルは Check(⋅,t,Π(P:Sort0).P→P) を呼び、まず Infer が走る。呼び出しは上と同じだが、最後の Infer(Γ2,P) だけが Sort0 を返す。つまり Infer は成功し、結果は
Infer(⋅,t)=Π(P:Sort0).Π(h:P).Sort0.
となる。項 t は正しく型付けされるが、主張された命題の証明にはなっていない。Check が二つの型を比較しても、どちらもこれ以上計算できず、いちばん内側の本体が片方では Sort0、もう片方では P なので、束縛変数を改名しても一致しない。そのため IsDefEq は失敗し、Check は型の不一致を報告する。(第 11 章 では、IsDefEq が使うアルゴリズムでこの比較を一歩ずつ再現する。)
この誤りが見つかったのは、利用者が証明すべき命題を宣言したからにほかならない。Infer だけを走らせるカーネルなら、t を受理し、黙って別の型を割り当てていただろう。どのスクリプトでも goal が先頭に来るのはこのためであり、qed がそれと照合するのもこのためである。
goal forall (P : Sort 0), P -> P
exact fun (P : Sort 0) => fun (h : P) => P
qed
仮定を使う: モーダス・ポネンス
ここまでの例では関数を一度も適用していないので、次の検査を考えよう。
t=λ(P:Sort0).λ(Q:Sort0).λ(f:P→Q).λ(p:P).fpagainstΠ(P:Sort0).Π(Q:Sort0).(P→Q)→P→Q,
この型は「P ならば Q であり、P が成り立つなら、Q が成り立つ」と述べている。四つの λ が、これまでと同じように文脈を Γ=P:Sort0,Q:Sort0,f:P→Q,p:P まで拡張し、本体 fp は Infer の適用のケースに達する。
- Infer(Γ,fp) T-App
- Infer(Γ,f) は P→Q、すなわち Π(_:P).Q を返す。Whnf はこれを変えず、これは Π である T-Var
- Check(Γ,p,P) は成功する。p:P が Γ にあるからである T-Var, T-Conv
- Q[_↦p]=Q を返す。名前 _ は Q に現れないので、代入は何もしない
利用者が代わりに ff と書いていたら、二つ目のステップで Check(Γ,f,P) が呼ばれ、P→Q は P ではないので失敗する。ここでは関数を自分自身に食わせることはできない。下で f p を f f に変えて試してほしい。
goal forall (P : Sort 0), forall (Q : Sort 0), (P -> Q) -> P -> Q
exact fun (P : Sort 0) => fun (Q : Sort 0) => fun (f : P -> Q) => fun (p : P) => f p
qed
全称命題の具体化
依存的な適用では、T-App の代入が実質的な仕事をする。最初の例の証明を id=λ(P:Sort0).λ(h:P).h とし、Γ=Q:Sort0 とする。このとき Infer(Γ,idQ) は次のようになる。
- Infer(Γ,idQ) T-App
- Infer(Γ,id) は Π(P:Sort0).Π(h:P).P を返す。すでに Π である T-Lam
- Check(Γ,Q,Sort0) は成功する T-Var
- (Π(h:P).P)[P↦Q]=Π(h:Q).Q、すなわち Q→Q を返す
論理的には、これは ∀ 除去であり、「どの命題も自分自身を含意する」から「Q は Q を含意する」を得ている。結果の型は引数 Q の値に依存し、依存 Π 型が表しているのはまさにこのことで、T-App が代入を行う理由でもある。エクスプローラでは、id は定義された定数で、Q は文脈から来る。T-App ノードの注記に代入が記録される。
def id : forall (P : Sort 0), P -> P := fun (P : Sort 0) => fun (h : P) => h
変換
第 3 章 の Id=λ(X:Sort0).X を思い出そう。核となる体系には定義がないので、IdP は項 (λ(X:Sort0).X)P の略記である。次の検査を考える。
t=λ(P:Sort0).λ(h:IdP).hagainstΠ(P:Sort0).P→P.
Infer はまず、注釈 IdP が型であることを確かめ、InferSort(Γ1,IdP) は Infer の適用のケースに達する。関数 λ(X:Sort0).X の型は Π(X:Sort0).Sort0 で、引数 P は Check(Γ1,P,Sort0) を通り、結果は Sort0[X↦P]=Sort0 である。したがって IdP は型であり、しかも命題である。本体 h の型は IdP なので、
Infer(⋅,t)=Π(P:Sort0).Π(h:IdP).IdP.
となる。束縛変数を改名するだけでは、これは Π(P:Sort0).P→P にならないが、IsDefEq は比較の前に両辺を計算する。IdP→βP なので、推論された型は Π(P:Sort0).Π(h:P).P になり、最初の例と同じく比較は成功する。型付け規則で言えば、このステップは導出の根における T-Conv の一回の使用である。
⋅⊢t:Π(P:Sort0).P→P⋅⊢t:Π(P:Sort0).IdP→IdP⋅⊢Π(P:Sort0).P→P:Sort0Π(P:Sort0).IdP→IdP≡Π(P:Sort0).P→P;(T-Conv)
アルゴリズムは、T-Conv をどこで適用するかを決して決めない。第 5 章 で述べたとおり、変換は IsDefEq の呼び出しの中に隠れており、二つの型が比較される場所でしか走らない。
ライブの導出を見ると、計算がどこで起きるかが分かる。注釈 IdP は T-App で検査され、推論された型には、Check がゴールと比較するまで IdP が残ったままである。
# h の注釈は (fun (X : Sort 0) => X) P であり、P に計算される。
goal forall (P : Sort 0), P -> P
exact fun (P : Sort 0) => fun (h : (fun (X : Sort 0) => X) P) => h
qed
演習
上の例のように Infer を手で走らせてみると、アルゴリズムの理解を確かめるよい練習になり、どれもプレイグラウンドで試せる。
Π(P:Sort0).Π(Q:Sort0).P→Q→P を証明せよ。
解答
一つの答えは λ(P:Sort0).λ(Q:Sort0).λ(p:P).λ(q:Q).p である。この証明は q を一度も使わないが、体系は仮定を使わずに済ませることを許している。
含意が推移的であることを証明せよ:
Π(P:Sort0).Π(Q:Sort0).Π(R:Sort0).(P→Q)→(Q→R)→P→R。
解答
一つの答えは λ(f:P→Q).λ(g:Q→R).λ(p:P).g(fp) で終わり、適用のケースを二回使う。まず fp:Q、次に g(fp):R である。
goal forall (P : Sort 0), forall (Q : Sort 0), forall (R : Sort 0), (P -> Q) -> (Q -> R) -> P -> R
exact fun (P : Sort 0) => fun (Q : Sort 0) => fun (R : Sort 0) => fun (f : P -> Q) => fun (g : Q -> R) => fun (p : P) => g (f p)
qed
λ(P:Sort0).λ(h:P).hh が拒否される理由と、アルゴリズム 5.1 のどの行で拒否されるかを説明せよ。
解答
文脈 P:Sort0,h:P において、本体 hh は適用のケースに達する。Infer は関数 h に対して P を返すが、Whnf(P)=P は変数であって Π ではないので、アルゴリズムは else fail "not a function" の行で失敗する。P の証明は適用できるものではない。Rust でのメッセージは application of non-function である。
覚えておくこと
- Infer の実行は導出木を映し、値を返す各呼び出しが一つのノードである。
- Infer は、宣言された定理を証明していない項に対しても成功しうるが、それを捕まえるのは、宣言された型に対する Check である。
- T-App は、仮定が使われる場所(モーダス・ポネンス)であり、全称命題が具体化される場所(Π の本体への代入)でもある。
- 型の内部の計算はすべて IsDefEq が扱い、アルゴリズムは T-Conv をどこで適用するかを決して選ばない。