カーネルが何かを検査するには、その前に正確な項の言語が要る。本書の言語には 5 種類の項があり、命題の上の量化を含む命題論理を表現するにはそれで足りる。本章では文法、本書の残りで使う記法の約束、そして項を表す Rust の型を定める。
5 種類の項
核となる言語は小さな依存型理論であり、その項は次の文法で生成される。
t , A , B : : = S o r t u ∣ x ∣ Π ( x : A ) . B ∣ λ ( x : A ) . t ∣ t s t, A, B \;::=\; \Sort{u} \;\mid\; x \;\mid\; \tPi{x}{A}{B} \;\mid\; \tLam{x}{A}{t} \;\mid\; t\;s t , A , B ::= Sort u ∣ x ∣ Π ( x : A ) . B ∣ λ ( x : A ) . t ∣ t s
ここで u u u は自然数、x x x は変数名である。各形には役割がある。
S o r t u \Sort{u} Sort u は宇宙 (universe)、すなわち要素がそれ自体型であるような型で、数 u u u はそのレベル である。S o r t 0 \Sort{0} Sort 0 は命題の宇宙である。宇宙が一つではなく階層全体になっている理由は第 13 章 で説明する。
x x x は変数 であり、その意味は現れる文脈、すなわちそれを囲む λ \lambda λ か Π \Pi Π の束縛子か、型付け文脈の仮定から決まる。
Π ( x : A ) . B \tPi{x}{A}{B} Π ( x : A ) . B は Π \Pi Π 型 、第 1 章 の依存関数型である。x : A x : A x : A を受け取って型 B B B のものを返す関数の型であり、B B B は x x x に言及してよい。
λ ( x : A ) . t \tLam{x}{A}{t} λ ( x : A ) . t は λ \lambda λ 抽象 で、型 A A A の引数 x x x を受け取って t t t を返す関数である。
t s t\;s t s は t t t の s s s への適用 である。
この文法は型と項を区別せず、命題 Π ( P : S o r t 0 ) . P → P \tPi{P}{\Sort{0}}{P \to P} Π ( P : Sort 0 ) . P → P もその証明 λ ( P : S o r t 0 ) . λ ( p : P ) . p \tLam{P}{\Sort{0}}{\tLam{p}{P}{p}} λ ( P : Sort 0 ) . λ ( p : P ) . p も、同じ 5 つの構成子から組み立てた項である。
λ ( P : S o r t 0 ) . λ ( p : P ) . p ⏟ proof : Π ( P : S o r t 0 ) . P → P ⏟ proposition, the type of the proof . \underbrace{\tLam{P}{\Sort{0}}{\tLam{p}{P}{p}}}_{\text{proof}}
\quad : \quad
\underbrace{\tPi{P}{\Sort{0}}{P \to P}}_{\text{proposition, the type of the proof}}. proof λ ( P : Sort 0 ) . λ ( p : P ) . p : proposition, the type of the proof Π ( P : Sort 0 ) . P → P .
この二つの項を木として見ると文法が共有されていることが目に見え、どの節点も 5 つの構成子のどれかで、Π \Pi Π や λ \lambda λ の節点に付いた名前はそれが束縛する変数である。節点をクリックすればそれが表す部分項が見えるし、別の項を入力してもよい。
項が妥当な型 であるかどうかは構文的な性質ではなく、型検査器が決める。項が型であるのは、その項自身の型が宇宙であるとき、そのときに限る(第 5 章 )。
第 II 部ではもう一つの形、定数 c c c を加える。定数はカーネルのシグネチャで宣言されたもの、たとえば N a t \mathsf{Nat} Nat や N a t . s u c c \mathsf{Nat.succ} Nat.succ を名指し、型が大域的な表から来る自由変数のように振る舞って、束縛されることはない。第 I 部の内容は定数なしで成り立つので、必要になる第 7 章 で導入するが、下の Rust の型には定数がすでに含まれている。
記法の約束
本書では次の文字を一貫して使う。
文字
意味
s , t , a , b , f s, t, a, b, f s , t , a , b , f および A , B , C , T A, B, C, T A , B , C , T (プライムや添字が付くこともある)
項
x , y , z x, y, z x , y , z および Π \Pi Π や λ \lambda λ で束縛される任意の名前
変数名
u , v , w u, v, w u , v , w
宇宙レベル
Γ \Gamma Γ
型付け文脈(第 3 章 )
Σ \Sigma Σ
定数のシグネチャ(第 7 章 )
項を読みやすくするために、三つの略記を使う。
->。 B B B が x x x に言及しないとき、Π ( x : A ) . B \tPi{x}{A}{B} Π ( x : A ) . B を A → B A \to B A → B と書く。したがってふつうの関数型は依存関数型の特別な場合であり、含意は全称量化の特別な場合である。実装では、-> の束縛子は予約された名前 _ である。
結合性。 適用は左に、-> は右に結合する。
f a b = ( f a ) b , A → B → C = A → ( B → C ) . f\;a\;b \;=\; (f\;a)\;b,
\qquad
A \to B \to C \;=\; A \to (B \to C). f a b = ( f a ) b , A → B → C = A → ( B → C ) .
カリー化。 二引数の関数とは、一つ目の引数を受け取って、二つ目を待つ関数を返す関数である。たとえば λ ( a : A ) . λ ( b : B ) . a \tLam{a}{A}{\tLam{b}{B}{a}} λ ( a : A ) . λ ( b : B ) . a は型 A → B → A A \to B \to A A → B → A を持ち、これを x : A x : A x : A と y : B y : B y : B に適用することを次のように書く。
( λ ( a : A ) . λ ( b : B ) . a ) x y = ( ( λ ( a : A ) . λ ( b : B ) . a ) x ) y . (\tLam{a}{A}{\tLam{b}{B}{a}})\;x\;y \;=\; ((\tLam{a}{A}{\tLam{b}{B}{a}})\;x)\;y. ( λ ( a : A ) . λ ( b : B ) . a ) x y = (( λ ( a : A ) . λ ( b : B ) . a ) x ) y .
二つの結合性の約束は噛み合うように決めてあり、型 A → B → C A \to B \to C A → B → C のカリー化された関数を f a b f\;a\;b f a b と適用するとき、どちらの側にも括弧は要らない。
カリー化のおかげで、言語全体が一引数関数だけで済む。A → B → C A \to B \to C A → B → C の証明は、A A A の証明を与えられると B → C B \to C B → C の証明を返す関数であり、その証明もまた関数である。「A A A と B B B を仮定する」は入れ子になった二つの仮定になり、証明項の二つの λ \lambda λ がそれを映している。
束縛とスコープ
λ ( x : A ) . t \tLam{x}{A}{t} λ ( x : A ) . t と Π ( x : A ) . B \tPi{x}{A}{B} Π ( x : A ) . B において、変数 x x x は束縛 されており、そのスコープ は本体 t t t または B B B であり、注釈 A A A はスコープの外にある。λ ( x : x ) . x \tLam{x}{x}{x} λ ( x : x ) . x では、注釈の中の x x x は項の外にある何かを指し、本体の x x x は引数を指す。自分を束縛する束縛子のスコープの中にない変数の出現は自由 である。たとえば λ ( x : P ) . f x y \tLam{x}{P}{f\;x\;y} λ ( x : P ) . f x y では、変数 P P P 、f f f 、y y y が自由で、x x x が束縛されている。
束縛変数については、本書全体で重要になる事実が二つあり、どちらも第 11 章 で形式化する。
束縛変数の名前には意味がなく、λ ( x : A ) . x \tLam{x}{A}{x} λ ( x : A ) . x と λ ( y : A ) . y \tLam{y}{A}{y} λ ( y : A ) . y は同じ関数である。束縛変数の名前だけが異なる項を α 同値 (alpha-equivalent)と呼び、カーネルはそれらを等しいものとして扱わなければならない。
変数を項で置き換える操作は型付け規則にも計算にも必要だが、その際に自由変数をうっかり同じ名前の束縛子の下に持ち込んではならない。これを正しく行うことを捕獲回避代入 (capture-avoiding substitution)と呼ぶ。
どちらの事実も述べるのは簡単だが、どちらも実際の証明検査器で本物のバグを引き起こしてきたからこそ、一章を割いている。
コードでは
項は src/term.rs の Rust の列挙型 Term であり、項の種類ごとにひとつのバリアントがあって、それに第 II 部の定数が加わる。
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Term {
Sort (u64), // Sort u
Var (String ), // x (local, bound or in the context)
Const (String ), // c (global, from the signature)
Pi (String , Box <Term >, Box <Term >), // Pi (x : A). B
Lam (String , Box <Term >, Box <Term >), // fun (x : A). b
App (Box <Term >, Box <Term >), // f a
}
変数は名前の文字列で表し、束縛子は束縛する名前を保持する。これはもっとも直接的で読みやすい表現であり、同時に第 11 章 の名前の問題を避けられなくする表現でもある。本書の目的はその問題を目にして解くことにあるので、それは意図したことである。
同じファイルには、入れ子の Box::new 呼び出しなしで項を書けるように、小さな構成関数も用意してある。var("x")、const_term("Nat")、app(f, a)、pi("x", a, b)、lam("x", a, b)、そして pi("_", a, b) を作る arrow(a, b) である。さらに三つの補助関数が、本章の約束をそのままコードにしている。
/// f a_1 ... a_n (left-associated application)
pub fn apps(f: Term , args: impl IntoIterator <Item = Term >) -> Term {
args.into_iter().fold(f, app)
}
/// Pi (x_1 : A_1) ... Pi (x_k : A_k). tail (a telescope of binders)
pub fn telescope(binders: &[(String , Term )], tail: Term ) -> Term {
binders.iter().rev().fold(tail, |body, (name, ty)| pi(name, ty.clone(), body))
}
/// Decomposes t into its spine: a head that is not an application, and its arguments.
/// f a b becomes (f, [a, b]); a non-application becomes (t, []).
pub fn spine(t: &Term ) -> (Term , Vec <Term >) {
match t {
Term ::App (f, a) => {
let (head, mut args) = spine(f);
args.push((**a).clone());
(head, args)
}
_ => (t.clone(), vec! []),
}
}
apps と spine は互いに逆であり、apps(h, args) が左に入れ子になった適用 h a 1 ⋯ a n h\;a_1 \cdots a_n h a 1 ⋯ a n を組み立て、spine がそれを再びばらす。第 12 章 の簡約エンジンは項をこの背骨(spine)の形で見るので、今のうちに慣れておいてほしい。入れ子の項 ( ( f a ) b ) c ((f\;a)\;b)\;c (( f a ) b ) c の頭部は f f f 、引数は [ a , b , c ] [a, b, c] [ a , b , c ] である。各適用の関数側をたどって下りていくと頭部に着き、途中で集めた引数を内側から外側へ読めば正しい順序になる。
本書全体で使う .leanmini スクリプトの表層構文では、同じ項を Sort 0、forall (x : A), B、A -> B、fun (x : A) => b と書き、適用は並置で表す。完全な文法は付録 E にある。
演習
命題「すべての命題 P P P と Q Q Q について、P P P ならば Q Q Q ならば P P P 」を項として書き、その証明項を与えよ。
解答
この命題は Π ( P : S o r t 0 ) . Π ( Q : S o r t 0 ) . P → Q → P \tPi{P}{\Sort{0}}{\tPi{Q}{\Sort{0}}{P \to Q \to P}} Π ( P : Sort 0 ) . Π ( Q : Sort 0 ) . P → Q → P であり、展開すると
Π ( P : S o r t 0 ) . Π ( Q : S o r t 0 ) . Π ( _ : P ) . Π ( _ : Q ) . P \tPi{P}{\Sort{0}}{\tPi{Q}{\Sort{0}}{\tPi{\_}{P}{\tPi{\_}{Q}{P}}}} Π ( P : Sort 0 ) . Π ( Q : Sort 0 ) . Π ( _ : P ) . Π ( _ : Q ) . P になる。
証明の一つは λ ( P : S o r t 0 ) . λ ( Q : S o r t 0 ) . λ ( p : P ) . λ ( q : Q ) . p \tLam{P}{\Sort{0}}{\tLam{Q}{\Sort{0}}{\tLam{p}{P}{\tLam{q}{Q}{p}}}} λ ( P : Sort 0 ) . λ ( Q : Sort 0 ) . λ ( p : P ) . λ ( q : Q ) . p である。leanmini の構文では次のようになる。
goal forall (P : Sort 0), forall (Q : Sort 0), P -> Q -> P
exact fun (P : Sort 0) => fun (Q : Sort 0) => fun (p : P) => fun (q : Q) => p
qed
f a ( g b ) c f\;a\;(g\;b)\;c f a ( g b ) c と A → ( B → C ) → D A \to (B \to C) \to D A → ( B → C ) → D に括弧を完全に付け、さらに前者の背骨を与えよ。
解答
f a ( g b ) c = ( ( f a ) ( g b ) ) c f\;a\;(g\;b)\;c = ((f\;a)\;(g\;b))\;c f a ( g b ) c = (( f a ) ( g b )) c で、頭部は f f f 、引数は [ a , g b , c ] [a,\; g\;b,\; c] [ a , g b , c ] である。括弧があるので g b g\;b g b は一つの引数のままであることに注意。
A → ( B → C ) → D = A → ( ( B → C ) → D ) A \to (B \to C) \to D = A \to ((B \to C) \to D) A → ( B → C ) → D = A → (( B → C ) → D ) で、これは Π ( _ : A ) . Π ( _ : ( Π ( _ : B ) . C ) ) . D \tPi{\_}{A}{\tPi{\_}{(\tPi{\_}{B}{C})}{D}} Π ( _ : A ) . Π ( _ : ( Π ( _ : B ) . C )) . D である。
λ ( x : A ) . λ ( y : x ) . λ ( x : S o r t 0 ) . y \tLam{x}{A}{\tLam{y}{x}{\tLam{x}{\Sort{0}}{y}}} λ ( x : A ) . λ ( y : x ) . λ ( x : Sort 0 ) . y において、x x x の各出現はどの束縛子を指すか。自由変数はどれか。
解答
y y y の注釈にある x x x は、内側の λ ( x : S o r t 0 ) \lambda(x : \Sort{0}) λ ( x : Sort 0 ) にはまだ入っていないので、外側の束縛子 λ ( x : A ) \lambda(x : A) λ ( x : A ) を指す。いちばん内側の x x x は内側の束縛子に束縛されているが、使われていない。自由変数は A A A だけである。この項は第 11 章 に再登場し、シャドーイングを雑に扱う型検査器を壊す。
覚えておくこと
項は宇宙 S o r t u \Sort{u} Sort u 、変数、Π \Pi Π 型、λ \lambda λ 抽象、適用であり、型と項は一つの文法を共有する。
A → B A \to B A → B は本体が変数を使わない Π \Pi Π 型の略記である。適用は左結合、-> は右結合で、関数はカリー化されている。
束縛変数の名前は任意であり、代入は捕獲を避けなければならず、どちらも第 11 章で扱う。
Rust では Term が形ごとに一つのバリアントを持つ列挙型であり、spine/apps が入れ子の適用と「頭部と引数のリスト」の間を相互に変換する。