h5i

第 2 章

項の言語

カーネルが何かを検査するには、その前に正確な項の言語が要る。本書の言語には 5 種類の項があり、命題の上の量化を含む命題論理を表現するにはそれで足りる。本章では文法、本書の残りで使う記法の約束、そして項を表す Rust の型を定める。

5 種類の項

核となる言語は小さな依存型理論であり、その項は次の文法で生成される。

t,A,B  ::=  Sort u  ∣  x  ∣  Π(x:A). B  ∣  λ(x:A). t  ∣  t  st, A, B \;::=\; \Sort{u} \;\mid\; x \;\mid\; \tPi{x}{A}{B} \;\mid\; \tLam{x}{A}{t} \;\mid\; t\;s

ここで uu は自然数、xx は変数名である。各形には役割がある。

  • Sort u\Sort{u} は宇宙(universe)、すなわち要素がそれ自体型であるような型で、数 uu はそのレベルである。Sort 0\Sort{0} は命題の宇宙である。宇宙が一つではなく階層全体になっている理由は第 13 章で説明する。
  • xx は変数であり、その意味は現れる文脈、すなわちそれを囲む λ\lambda か Π\Pi の束縛子か、型付け文脈の仮定から決まる。
  • Π(x:A). B\tPi{x}{A}{B} は Π\Pi 型、第 1 章の依存関数型である。x:Ax : A を受け取って型 BB のものを返す関数の型であり、BB は xx に言及してよい。
  • λ(x:A). t\tLam{x}{A}{t} は λ\lambda 抽象で、型 AA の引数 xx を受け取って tt を返す関数である。
  • t  st\;s は tt の ss への適用である。

この文法は型と項を区別せず、命題 Π(P:Sort 0). P→P\tPi{P}{\Sort{0}}{P \to P} もその証明 λ(P:Sort 0). λ(p:P). p\tLam{P}{\Sort{0}}{\tLam{p}{P}{p}} も、同じ 5 つの構成子から組み立てた項である。

λ(P:Sort 0). λ(p:P). p⏟proof:Π(P:Sort 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}}.

この二つの項を木として見ると文法が共有されていることが目に見え、どの節点も 5 つの構成子のどれかで、Π\Pi や λ\lambda の節点に付いた名前はそれが束縛する変数である。節点をクリックすればそれが表す部分項が見えるし、別の項を入力してもよい。

項が妥当な型であるかどうかは構文的な性質ではなく、型検査器が決める。項が型であるのは、その項自身の型が宇宙であるとき、そのときに限る(第 5 章)。

記法の約束

本書では次の文字を一貫して使う。

文字 意味
s,t,a,b,fs, t, a, b, f および A,B,C,TA, B, C, T(プライムや添字が付くこともある) 項
x,y,zx, y, z および Π\Pi や λ\lambda で束縛される任意の名前 変数名
u,v,wu, v, w 宇宙レベル
Γ\Gamma 型付け文脈(第 3 章)
Σ\Sigma 定数のシグネチャ(第 7 章)

項を読みやすくするために、三つの略記を使う。

->。 BB が xx に言及しないとき、Π(x:A). B\tPi{x}{A}{B} を A→BA \to 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).

カリー化。 二引数の関数とは、一つ目の引数を受け取って、二つ目を待つ関数を返す関数である。たとえば λ(a:A). λ(b:B). a\tLam{a}{A}{\tLam{b}{B}{a}} は型 A→B→AA \to B \to A を持ち、これを x:Ax : A と y:By : 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→B→CA \to B \to C のカリー化された関数を f  a  bf\;a\;b と適用するとき、どちらの側にも括弧は要らない。

束縛とスコープ

λ(x:A). t\tLam{x}{A}{t} と Π(x:A). B\tPi{x}{A}{B} において、変数 xx は束縛されており、そのスコープは本体 tt または BB であり、注釈 AA はスコープの外にある。λ(x:x). x\tLam{x}{x}{x} では、注釈の中の xx は項の外にある何かを指し、本体の xx は引数を指す。自分を束縛する束縛子のスコープの中にない変数の出現は自由である。たとえば λ(x:P). f  x  y\tLam{x}{P}{f\;x\;y} では、変数 PP、ff、yy が自由で、xx が束縛されている。

束縛変数については、本書全体で重要になる事実が二つあり、どちらも第 11 章で形式化する。

  1. 束縛変数の名前には意味がなく、λ(x:A). x\tLam{x}{A}{x} と λ(y:A). y\tLam{y}{A}{y} は同じ関数である。束縛変数の名前だけが異なる項を α 同値(alpha-equivalent)と呼び、カーネルはそれらを等しいものとして扱わなければならない。
  2. 変数を項で置き換える操作は型付け規則にも計算にも必要だが、その際に自由変数をうっかり同じ名前の束縛子の下に持ち込んではならない。これを正しく行うことを捕獲回避代入(capture-avoiding substitution)と呼ぶ。

どちらの事実も述べるのは簡単だが、どちらも実際の証明検査器で本物のバグを引き起こしてきたからこそ、一章を割いている。

コードでは

同じファイルには、入れ子の 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  a1⋯anh\;a_1 \cdots a_n を組み立て、spine がそれを再びばらす。第 12 章の簡約エンジンは項をこの背骨(spine)の形で見るので、今のうちに慣れておいてほしい。入れ子の項 ((f  a)  b)  c((f\;a)\;b)\;c の頭部は ff、引数は [a,b,c][a, b, c] である。各適用の関数側をたどって下りていくと頭部に着き、途中で集めた引数を内側から外側へ読めば正しい順序になる。

本書全体で使う .leanmini スクリプトの表層構文では、同じ項を Sort 0、forall (x : A), B、A -> B、fun (x : A) => b と書き、適用は並置で表す。完全な文法は付録 Eにある。

演習

命題「すべての命題 PP と QQ について、PP ならば QQ ならば PP」を項として書き、その証明項を与えよ。

解答

この命題は Π(P:Sort 0). Π(Q:Sort 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\tPi{P}{\Sort{0}}{\tPi{Q}{\Sort{0}}{\tPi{\_}{P}{\tPi{\_}{Q}{P}}}} になる。 証明の一つは λ(P:Sort 0). λ(Q:Sort 0). λ(p:P). λ(q:Q). p\tLam{P}{\Sort{0}}{\tLam{Q}{\Sort{0}}{\tLam{p}{P}{\tLam{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)  cf\;a\;(g\;b)\;c と A→(B→C)→DA \to (B \to C) \to D に括弧を完全に付け、さらに前者の背骨を与えよ。

解答

f  a  (g  b)  c=((f  a)  (g  b))  cf\;a\;(g\;b)\;c = ((f\;a)\;(g\;b))\;c で、頭部は ff、引数は [a,  g  b,  c][a,\; g\;b,\; c] である。括弧があるので g  bg\;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\tPi{\_}{A}{\tPi{\_}{(\tPi{\_}{B}{C})}{D}} である。

λ(x:A). λ(y:x). λ(x:Sort 0). y\tLam{x}{A}{\tLam{y}{x}{\tLam{x}{\Sort{0}}{y}}} において、xx の各出現はどの束縛子を指すか。自由変数はどれか。

解答

yy の注釈にある xx は、内側の λ(x:Sort 0)\lambda(x : \Sort{0}) にはまだ入っていないので、外側の束縛子 λ(x:A)\lambda(x : A) を指す。いちばん内側の xx は内側の束縛子に束縛されているが、使われていない。自由変数は AA だけである。この項は第 11 章に再登場し、シャドーイングを雑に扱う型検査器を壊す。

覚えておくこと

  • 項は宇宙 Sort u\Sort{u}、変数、Π\Pi 型、λ\lambda 抽象、適用であり、型と項は一つの文法を共有する。
  • A→BA \to B は本体が変数を使わない Π\Pi 型の略記である。適用は左結合、-> は右結合で、関数はカリー化されている。
  • 束縛変数の名前は任意であり、代入は捕獲を避けなければならず、どちらも第 11 章で扱う。
  • Rust では Term が形ごとに一つのバリアントを持つ列挙型であり、spine/apps が入れ子の適用と「頭部と引数のリスト」の間を相互に変換する。