恒等関数と第 1 引数の射影
id は受け取った引数をそのまま返します。keep は同じ型の引数を 2 つ受け取り、第 1 引数(先に渡された値)を返します。
axiom A : Type;
def id (x : A) : A := x;
theorem keep (x y : A) : A := x;
読み込み中…
Proofglyph の .pg ファイルでは、型と証明項を最小限の記号列で記述します。検査器はその記号列を読み込み、各証明項が指定された型を持つかを検証します。検査の体系は、LF(Logical Framework)の型理論である λΠ 計算(依存型付きラムダ計算)に基づいています。
LF は、多様な論理の構文・推論規則・証明を依存型付きラムダ計算上で統一的に表現・検証するためのメタ言語です。Robert Harper、Furio Honsell、Gordon Plotkin が論文 A Framework for Defining Logics(1987 / 1993)において提案しました。
自然演繹やシーケント計算、集合論など、論理体系ごとに記号や推論規則は異なります。LF では、これらを「シグネチャ」と呼ばれる定数宣言の列として定義します。検査器はその宣言をもとに、関数の引数・戻り値の型、変数のスコープ、型の同値性を判定します。
例えば高水準言語 Hieratic では、命題の型 Prop と、命題からその証明の型への写像 Prf(カインド Prop -> Type)を次のように宣言します。命題 P は型 Prop を持ち、その証明は型 Prf P を持つ項として表現されます。このコードをコンパイルして Proofglyph 形式で検査します。
axiom Prop : Type;
axiom Prf : Prop -> Type;
axiom P : Prop;
theorem identity (p : Prf P) : Prf P := p;
このコードをコンパイルすると、次の .pg が生成されます。
*:
#0*>:
#0:
#1#2@#1#2@>#1#2@$0\!
上記の Proofglyph コードにおいて、#0 は Prop、#1 は Prf、#2 は P を指します。演算子 : で前提(公理)を登録し、最後の ! で identity の型と証明項を検査して登録します。
定理 identity は「P の証明を受け取ってそのまま返す関数」です。引数として P の証明を必要とするため、これは P を無条件に証明したのではなく、含意 P → P に相当する仮定付きの証明です。自然演繹における仮定の導入と解消が、ラムダ計算における関数の引数受け取りとラムダ抽象に対応しています。
Prf P は、命題 P の値に依存する型(依存型 / 型族)です。forall (P : Prop), Prf P -> Prf P は、任意の命題 P とその証明を受け取り、同じ命題の証明を返す関数の型を表します。Hieratic では forall 構文を用いて、引数の値に依存する関数型(依存積型 / Π型)を記述します。
| 役割 | 例 |
|---|---|
| 項(Object) | P : Prop、p : Prf P |
| 型 / 型族(Type / Family) | Prop : Type、Prf P : Type |
| カインド(Kind) | Type、Prop -> Type。いずれも Kind に分類されます。Kind 自体には型が存在しません。 |
対象論理の全称量化子を All : (U -> Prop) -> Prop と宣言すると、「すべての x について F x」という命題は All (fun (x : U) => F x) と表現できます。このように、対象論理の変数束縛を LF のラムダ束縛で表現する手法を HOAS(Higher-Order Abstract Syntax) と呼びます。Proofglyph の内部表現では de Bruijn インデックス を採用し、変数名の違い(α 同値性)の吸収や変数捕獲を防ぐ代入を安全かつ決定論的に行います。
型検査器の役割は、宣言された前提のもとで証明項が指定された型を持つかを機械的に確かめることです。前提(公理系)自体の無矛盾性や、記述した規則が意図した対象論理を正確に捉えているかは、型検査器の保証範囲外です。LF の元論文では、対象論理の構文・演繹木と LF の正準項の 1 対 1 対応が「適合性(Adequacy)」として証明されています。
Proofglyph のカーネルは、Π型、λ項、関数適用、ソート(Type / Kind)、変数、不透明な定数のみを扱う最小構成です。型の同値判定には β 簡約のみを用います。論理の推論規則や公理はすべて前提として宣言し、証明項は明示的に記述します(自動証明探索、タクティクス、暗黙引数などの機能はありません)。
Hieratic(.ht)は変数名や定数名を用いて人間が読み書きしやすい形式で記述する高水準言語であり、Proofglyph(.pg)は型と証明項を最小限の記号列で記述するスタック指向の中間言語です。Hieratic で記述されたコードは Proofglyph へとコンパイルされ、カーネルによって型検査されます。Proofglyph を直接入力して検査することも可能です。
Proofglyph は記号列を先頭から順に走査し、構文要素をスタックへ積んで式を組み立てます。二項演算子 >、\、@ はスタックトップから 2 つの要素を取り出し、新たな型や項を構築してスタックへプッシュします(表中の A B や f x は、右側の要素がスタックの最上位を示します)。
| 記号 | 操作 |
|---|---|
* | Type をプッシュ |
^ | Kind をプッシュ |
$n | 変数(de Bruijn インデックス、$0 が最も内側の引数) |
#n | 登録済み定数(0 から始まる連番) |
> | スタックから A と B を取り出し、引数の型が A、戻り値の型が B である依存関数型(Π型)を積む(B の内部から引数を参照可能) |
\ | スタックから A と b を取り出し、型 A の引数を受け取って b を返す関数(λ項)を積む |
@ | スタックから f と x を取り出し、関数 f を引数 x に適用した項(関数適用)を積む |
: | スタックが [型](1 要素のみ)のとき、その型を持つ前提(公理)を定数として登録し、スタックを空にする |
! | スタックが [型, 項](2 要素のみ)のとき、項がその型を持つかを型検査して証明済み定数として登録し、スタックを空にする |
型 A と B、関数 f : A -> B を前提として、引数 x : A に f を適用する関数を書きます。
*:
*:
#0#1>:
#0#1>#0#2$0@\!
最初の二つの *: で A(#0)と B(#1)を登録します。#0#1>: は関数型 A -> B を作り、その型を持つ f(#2)を前提として登録します。#0#1>: を読むときのスタックは次のように変わります。
図では上がスタックの最上部です。> は二つの項から関数型を作り、: はその型を持つ前提を登録してスタックを空にします。
最後の行では、#0#1> で結論の型 A -> B を積みます。続く #0#2$0@\ が fun (x : A) => f x に対応します。#2$0@ は f を引数 x に適用し、\ は型 A の引数を受け取る関数を作ります。末尾の ! で型と本体を検査し、成功すると証明済み定数 #3 を登録します。
変数 $0 は最も内側のバインダの引数、$1 はその一つ外側の引数を表します。この変数番号付け方式を de Bruijn インデックス と呼びます。例えば #0#0$1\\ は、型 #0 の引数を 2 つ受け取り、第 1 引数(先に渡された値)を返す関数を表します。
定数番号 #0, #1, … は登録順に 0 から連番で割り振られます。参照できるのは、それ以前に登録済みの定数のみです(前方参照や自己参照は不可)。
トークン間の空白や改行は自由に省略・挿入できますが、$ 0 や # 12 のようにプレフィックス記号と数字の間に空白を入れることはできません($0 や #12 のように詰めて記述します)。なお、Proofglyph にはコメント構文は存在しません。
型検査は : および ! による定数登録のタイミングで実行されます。操作に必要なスタック要素が不足している場合、登録時にスタックに余分な項が残っている場合、ファイル終端(EOF)でスタックが空でない場合などはすべて構文エラーとなります。
Hieratic では、Proofglyph のような数値インデックスの代わりに、識別子(変数名・定数名)を用いて人間が読み書きしやすい形式で記述します。関数型は A -> B、関数適用は f x のように表現し、コンパイル時に名前解決が行われて番号へと変換されます。
| 構文 | 説明 |
|---|---|
axiom 名前 : 型; | 公理(前提)の宣言 |
def 名前 : 型 := 項; | 定義の登録 |
theorem 名前 : 型 := 証明項; | 定理の証明(内部的には def と同一) |
| 構文 | 説明 |
|---|---|
Type / Kind | Type は通常の型の型(ソート)。Type : Kind が成り立つ。Kind 自体には型が存在しない |
f x y | 関数適用(左結合: (f x) y) |
A -> B | 関数型(右結合: A -> (B -> C)) |
forall (x : A), B | 依存関数型(Π型) |
fun (x : A) => b | ラムダ抽象(λ項) |
axiom A : Type;
def id (x : A) : A := x;
このコードは *:#0#0>#0$0\! に変換されます。A が定数 #0 に、関数の引数 x が変数 $0 に対応します。定数名 id は Proofglyph の記号列には含まれません。
宣言の引数リストは、型側では forall、本体側では fun へと展開(糖衣構文展開)されます。上記の定義は次のように書くことと同値です。
def id : A -> A := fun (x : A) => x;
関数適用 f x y は左結合((f x) y)、関数型 A -> B -> C は右結合(A -> (B -> C))として解釈されます。依存関数型 forall (x : A), B では、戻り値の型 B の内部で引数 x を参照できます。
付属の証明例では、Prop を命題の型、Prf P を命題 P の証明を表す型として扱っています。theorem 名前 : Prf P := 項; は、与えられた証明項が型 Prf P を持つかを型検査します。論理の推論規則やペアノの公理などは、すべて前提(axiom)として宣言します。
def と theorem は機能的に同一であり、どちらも型検査を経て Proofglyph の ! に変換されます。登録済み定数の定義本体は型比較時にも展開されません(不透明な定数 / opaque constants)。そのため、インラインで直接記述したラムダ式 (fun (x : A) => x) a は a へ β 簡約されますが、登録済みの定数適用 id a は展開されません。
検査結果の「検査成功」は、入力に含まれる前提のもとで証明項の型付けが妥当であることを意味します。前提(公理)自体の正しさや無矛盾性までは判定しません。
識別子は正規表現 [A-Za-z_][A-Za-z0-9_']* で表されます。axiom, def, theorem, import, forall, fun, Type, Kind は予約語です。アンダースコア _ も通常の識別子名であり、型推論の穴(ワイルドカード)ではありません。また、数値リテラルや中置の算術演算子(+ や * など)は言語機能として組み込まれていません。
名前解決は最も内側の引数から外側へ向かって探索し、見つからなければ先行する大域宣言を検索します。内側の引数名で外側の名前を覆い隠すシャドーイング(shadowing)は可能です。大域的な宣言名の重複、前方参照(未定義の参照)、自己参照(再帰定義)はエラーとなります。引数の省略記法 (x y : A) は (x : A) (y : A) に展開されます。
import "commons/logic/propositional.ht";
CLI の compile コマンドは、インポート元のファイルを基準に相対パスを解決し、依存先ファイルの宣言をその位置へインライン展開します。同一の実体ファイルは重複して読み込まれません。循環インポート、存在しないファイル、インポート展開後の名前の重複はエラーとなります(名前空間や別名定義の機能はありません)。
// から行末まではコメントとして扱われます。引数はすべて明示的に渡す必要があります(型推論による穴やタクティクスはありません)。なお、この Web デモでは単一ファイル内での編集となるため、証明例には依存する前提宣言があらかじめすべてインラインで展開されています。
Hieratic を編集した場合は「コンパイルして検査」を、Proofglyph を直接編集した場合は「記号列を検査」を押してください。Proofglyph を直接編集・検査した場合は、ソースマップが存在しないため定数名が c0, c1, … として表示されます。
| 構成 | 条件・型 |
|---|---|
Type | Kind 型を持つ。Kind 自体には型が存在しない。 |
forall (x : A), B | A : Type が必要。x : A のもとで B : Type または B : Kind が整形式であること。 |
fun (x : A) => b | b : B ならば、形成可能な forall (x : A), B 型を持つ。 |
f u | f : forall (x : A), B かつ u : A ならば、型は B 中の x を u に代入(置換)したもの。 |
束縛変数として導入できるのは、型 Type を持つ項に限られます(一階依存型)。Type : Kind であるため、forall (A : Type), A -> A のようなカインドの抽象化(多相型)は拒否されます(公理として固定された特定の A : Type に対する恒等関数は記述可能です)。
型の同値性判定には完全な β 簡約 を用います。η 同値、定数の展開、証明無関係性(proof irrelevance)、ユーザー定義の書換え規則などはサポートしていません。また、自然数や帰納型は言語に組み込まれておらず、すべてシグネチャ上の前提として公理化します。
無限ループによるブラウザの停止を防ぐため、型検査の簡約ステップ数には計算上限(fuel)が設定されています。fuel を使い切ると検査は中断されますが、これは計算上限に達したことを意味し、直ちに証明項が誤っていることを示すわけではありません(CLI では --fuel N オプションで上限を変更可能です)。
以下の各例は単独で実行可能です。「エディタで試す」をクリックすると、エディタの内容が該当コードに置き換わり、コンパイルと型検査が実行されます。
id は受け取った引数をそのまま返します。keep は同じ型の引数を 2 つ受け取り、第 1 引数(先に渡された値)を返します。
axiom A : Type;
def id (x : A) : A := x;
theorem keep (x y : A) : A := x;
項 ab x は型 B を持ち、それに bc を適用した bc (ab x) は型 C を持ちます。命題の証明を引数にとれば、同一の構造で含意の推移律(三段論法)を導出できます。
axiom A : Type;
axiom B : Type;
axiom C : Type;
theorem compose (ab : A -> B) (bc : B -> C)
(x : A) : C := bc (ab x);
型族 B のカインドは A -> Type であり、引数 x : A に応じて B x が具体的な型となります。dependent_id は y : B x を受け取ってそのまま返す依存型の恒等関数です。
axiom A : Type;
axiom B : A -> Type;
theorem dependent_id (x : A) (y : B x) : B x := y;
Imp P Q は対象論理における含意命題、Prf P -> Prf Q は仮定 P の証明から Q の証明を導く LF の関数型です。imp_i はその証明変換関数から含意の証明を導出し(含意の導入規則)、imp_e は含意の証明と前提の証明から結論の証明を導出します(含意の除去規則 / モーダス・ポネンス)。
axiom Prop : Type;
axiom Prf : Prop -> Type;
axiom Imp : Prop -> Prop -> Prop;
axiom imp_i (P Q : Prop) :
(Prf P -> Prf Q) -> Prf (Imp P Q);
axiom imp_e (P Q : Prop) :
Prf (Imp P Q) -> Prf P -> Prf Q;
theorem self_imp (P : Prop) : Prf (Imp P P) :=
imp_i P P (fun (p : Prf P) => p);
theorem apply_self (P : Prop) (p : Prf P) : Prf P :=
imp_e P P (self_imp P) p;
戻り値の型 (fun (x : A) => B x) a は β 簡約によって B a と同値になるため、型 B a を持つ b をそのまま関数本体として返すことができます。
axiom A : Type;
axiom B : A -> Type;
axiom a : A;
axiom b : B a;
theorem beta : (fun (x : A) => B x) a := b;
エディタ上部の「証明例」ドロップダウンから、命題論理、等式の性質、自然数(ペアノ公理)、整除性、二項定理、素数の無限性、$\sqrt{2}$ の無理性、フェルマーの小定理、ラッセルのパラドックス(普遍集合の非存在)などを選択できます。依存する前提宣言を含めて展開され、選択した主要定理が検査結果の先頭に表示されます。
基本型、型族、公理、推論規則はすべて axiom として宣言します。これらはシグネチャ上の前提となります。導出したい結論を証明する際は theorem(または def)を用います。結論そのものを axiom で宣言してしまうと、証明ではなく単なる仮定になってしまう点に注意してください。
axiom A : Type;
axiom B : Type;
型 A -> B -> A の項を構成するには、x : A と y : B を引数として受け取り、型 A の項を返す関数を記述します。関数本体として x を返せば、この型を満たす証明項となります。
theorem first (x : A) (y : B) : A := x;
// 同じ宣言を引数の省略形なしで書くと:
// theorem first : A -> B -> A :=
// fun (x : A) (y : B) => x;
項 f : A -> B が存在する場合、x : A に対する関数適用 f x によって型 B の項が得られます。依存型理論に基づく規則では、命題や対象に関する引数も明示的に渡す必要があります。たとえば「例」タブの imp_e P P (self_imp P) p では、対象となる 2 つの命題 P, P、含意の証明 self_imp P、そして前提の証明 p を順番に渡しています。
Hieratic を編集した後は「コンパイルして検査」を押してください。検査に成功した宣言は、以降の証明から名前で参照できるようになります。複雑な証明は補題ごとに theorem として分割し、依存順(トポロジカル順)に並べます。エディタの入力を変更すると前回の検査結果はクリアされるため、編集完了後に再度検査を実行してください。
| エラーの種類 | 確認する点 |
|---|---|
| 構文エラー | 宣言末尾のセミコロン ;、型注釈のコロン :、定義本体の :=、括弧 () の対応関係を確認してください。 |
| 名前が見つからない | 識別子の綴りと宣言の順序を確認してください。未宣言の定数、後続の宣言、および自分自身(再帰)は参照できません。 |
| 引数の型が合わない | 規則の型定義を先頭から確認し、渡している引数の数・順序・型が合致しているか確認してください(暗黙引数はないため、命題引数などもすべて明示が必要です)。 |
| 証明の型が合わない | 宣言した結論の型と、証明項本体から推論された型を比較してください。tinyprover では登録済み定数の本体を展開しないため、定義の展開に依存した型一致は行われません。 |
| Π型を形成できない | 引数の型が Type であるか確認してください。カインドである Type そのものを引数として束縛する (A : Type) は形成できません。 |
| Proofglyph のスタックエラー | :(前提登録)の実行時はスタックに型 1 つのみ、!(定理検査)の実行時はスタックに型と項の 2 つのみが存在する必要があります。余分な項が残っているか、スタックが不足しています。 |
def id (x : A) : A := x; を登録しても、定数は不透明(opaque)として扱われるため、id a は a へと定義的に簡約されません。型レベルの計算で β 簡約を利用したい場合は、定数呼び出しではなくインラインのラムダ式を直接記述します。式の変形や等価性の移送には、定義の展開ではなく等式に関する証明項(置換規則など)を使用します。