h5i

第 12 章

簡約と変換

第 11 章では名前を扱ったのに対し、本章では計算を扱う。三種類の簡約ステップ、定義的等価の定義、そしてアルゴリズム 5.1 が未定義のまま残した Whnf、Normalize、IsDefEq のアルゴリズムである。設計上の要点は、帰納型の計算規則をシグネチャにデータとして保存することにあり、これにより、一つの簡約関数が、これまでに宣言されたあらゆる帰納型に対して働く。

簡約

λ\lambda 抽象を引数に適用した結果は、束縛変数に引数を代入して計算する。

(λ(x:A). t)  s  →β  t[x↦s].(\tLam{x}{A}{t})\;s \;\betared\; \subst{t}{x}{s}.

このステップを β 簡約と呼び、(λ(x:A). t)  s(\tLam{x}{A}{t})\;s の形の部分項を β 簡約基(beta-redex)と呼ぶ。第 II 部ではさらに二種類のステップを加えた。定義を展開する δ 簡約(第 7 章)と、構成子に対して再帰子を走らせる ι 簡約(図 7.1 と 9.1、および 第 8 章 の一般規則)である。各ステップは項の内部のどこで行ってもよいものとし、いずれかの種類の一回のステップを →\to と書く。どのステップも適用できない項は正規形にある。たとえば Id=λ(X:Sort 0). X\mathit{Id} = \tLam{X}{\Sort{0}}{X} とすると、

λ(h:Id  P). (λ(y:P). y)  h  →β  λ(h:P). (λ(y:P). y)  h  →β  λ(h:P). h,\tLam{h}{\mathit{Id}\;P}{(\tLam{y}{P}{y})\;h} \;\betared\; \tLam{h}{P}{(\tLam{y}{P}{y})\;h} \;\betared\; \tLam{h}{P}{h},

であり、最後の項は正規形である。内側の簡約基を先に簡約しても同じ結果に至る。

変換

定義的等価は変換(conversion)とも呼ばれ、束縛変数の名前を除いて同じ結果に計算される項どうしを同一視する。

定義 12.1(変換). s≡ts \conv t は、=α\alphaeq と →\to を含み、すべての項構成子について閉じた最小の同値関係である。

s≡ts \conv t を判定するために、カーネルは両方の項を正規化し、結果を =α\alphaeq で比較する。この手続きは二つの事実に依拠している。

  • 簡約は合流的であり、簡約の順序が違っても、異なる正規形に至ることはない。したがって s≡ts \conv t が成り立つのは、ss と tt の正規形が α 同値であるとき、かつそのときに限り、巧妙なステップ列を探し回る必要はない。
  • 型の付くすべての項は正規形を持つので、正規化は停止する。これは本書の体系で成り立つが、深い定理である(強正規化、第 14 章)[Barendregt 1992]。

実用的なカーネルは簡約ステップ数にも上限を設け、バグがあっても無限ループに陥らないようにする。本書の実装は 20,000 ステップで停止し、「reduction fuel exhausted」というエラーを出す。

データとしての計算規則

図 7.1 と 9.1 では ι 規則を一本ずつ別々の等式として書いており、これを見ると、新しい帰納型ごとにカーネルに新しい簡約コードが要るように思えるが、そうではない。カーネルは ι 規則を再帰子の隣にデータとしてシグネチャに保存し、一つのコードがすべての帰納型に対してそれを適用する。

各再帰子の項目は、大前提の位置、つまりどの規則を適用するかをその構成子が決める引数の位置と、その帰納型の構成子を記録する。各構成子について、カーネルは先頭の引数のうちいくつがパラメータ(規則はこれを無視する)で、いくつがフィールド(規則はこれを引き渡す)か、そしてどのフィールドが再帰的かを知っている。本書の再帰子のうち三つについて、そのデータと、結果として得られる右辺を次に示し、λ\lambda の型注釈は省略する。

RecursorMajorConstructorParams, fieldsRight-hand sideNat.recu4thzero0,  0λM z s.  zsucc0,  1λM z s n.  s  n  (Nat.recu  M  z  s  n)Eq.recu,v6threflu2,  0λA a M m b.  mTree.recv5thleaf0,  0λM m1 m2 m3.  m1node0,  1λM m1 m2 m3 t.  m2  t  (Tree.recv  M m1 m2 m3  t)branch0,  2λM m1 m2 m3 l r.  m3  l  r  (Tree.recv  M m1 m2 m3  l)  (⋯  r)\begin{array}{llcll} \textbf{Recursor} & \textbf{Major} & \textbf{Constructor} & \textbf{Params, fields} & \textbf{Right-hand side} \\[2pt] \NatRec{u} & \text{4th} & \zero & 0,\;0 & \lambda M\,z\,s.\; z \\[2pt] & & \suc & 0,\;1 & \lambda M\,z\,s\,n.\; s\;n\;(\NatRec{u}\;M\;z\;s\;n) \\[2pt] \EqRecDep{u,v} & \text{6th} & \refl{u} & 2,\;0 & \lambda A\,a\,M\,m\,b.\; m \\[2pt] \mathsf{Tree.rec}_v & \text{5th} & \mathsf{leaf} & 0,\;0 & \lambda M\,m_1\,m_2\,m_3.\; m_1 \\[2pt] & & \mathsf{node} & 0,\;1 & \lambda M\,m_1\,m_2\,m_3\,t.\; m_2\;t\;(\mathsf{Tree.rec}_v\;M\,m_1\,m_2\,m_3\;t) \\[2pt] & & \mathsf{branch} & 0,\;2 & \lambda M\,m_1\,m_2\,m_3\,l\,r.\; m_3\;l\;r\;(\mathsf{Tree.rec}_v\;M\,m_1\,m_2\,m_3\;l)\;(\cdots\;r) \end{array}

右辺は、大前提より前の引数を取り、次に構成子のフィールドを取る。たとえば Nat.recu  M  z  s  (succ  n)\NatRec{u}\;M\;z\;s\;(\suc\;n) は二番目の規則を選び、

(λM z s n.  s  n  (Nat.recu  M  z  s  n))  M  z  s  n,(\lambda M\,z\,s\,n.\; s\;n\;(\NatRec{u}\;M\;z\;s\;n))\;M\;z\;s\;n,

となり、これを β 簡約すると図 7.1 の二番目の等式の右辺になる。同様に、Eq.recu,v\EqRecDep{u,v} の唯一の規則は Eq.recu,v  A  a  M  m  b  (reflu  A′  c)\EqRecDep{u,v}\;A\;a\;M\;m\;b\;(\refl{u}\;A'\;c) に対して発火する。構成子 reflu\refl{u} は二つのパラメータ A′A' と cc を持ち、フィールドを持たないので、右辺は A  a  M  m  bA\;a\;M\;m\;b だけを受け取って mm を返す。

弱頭正規形と正規化

アルゴリズム 12.1 は、アルゴリズム 5.1 が未定義のまま残した三つの関数を定義する。Whnf\Whnf と Normalize\Normalize はどちらも項を背骨(spine)h  a1⋯anh\;a_1 \cdots a_n、つまり適用ではない頭部 hh に n≥0n \ge 0 個の引数を適用したものとして見る(第 2 章)。どの項もこの分解をちょうど一つ持つ。

アルゴリズム 12.1. β、δ、ι による弱頭正規化。完全正規化。定義的等価。

Whnf(t):
  loop:
    write t as a spine h a_1 ... a_n
    if h = λ(x : A). b and n ≥ 1:                               -- beta
        t ← (b[x ↦ a_1]) a_2 ... a_n
    else if h = c and (c : C := d) ∈ Σ:                         -- delta
        t ← d a_1 ... a_n
    else if h = c is a recursor with major position p ≤ n:      -- iota
        k b_1 ... b_j ← Whnf(a_p)
        if k is a constructor of c's inductive type with q parameters:
            t ← rhs_k a_1 ... a_{p-1} b_{q+1} ... b_j a_{p+1} ... a_n
        else return t                                           -- stuck, e.g. on a variable
    else return t

Normalize(t):
  t ← Whnf(t)
  if t = Π(x : A). B:  return Π(x : Normalize(A)). Normalize(B)
  if t = λ(x : A). b:  return λ(x : Normalize(A)). Normalize(b)
  else write t as a spine h a_1 ... a_n and return h Normalize(a_1) ... Normalize(a_n)

IsDefEq(s, t):
  return  · ; · ⊢ Normalize(s) =α Normalize(t)                  -- Figure 11.1

Whnf\Whnf は項の頭部でのみ簡約し、そこに適用できる規則がなくなるまで続け、結果が弱頭正規形である。λ\lambda や Π\Pi の内側、行き詰まった頭部の引数の中は決して見ない。呼び出し側が型の最も外側の形だけを必要とするとき、たとえば Infer\Infer の適用の場合で Π\Pi が欲しいときには、これで十分であり、完全な正規化より安くつく。Normalize\Normalize はまず弱頭正規形を計算し、次に直下の部分項をすべて正規化する。たとえば t=λ(h:Id  P). ht = \tLam{h}{\mathit{Id}\;P}{h} とし、Id\mathit{Id} を上のとおりとすると、

Whnf(t)=t,Normalize(t)=λ(h:P). h,\Whnf(t) = t, \qquad \Normalize(t) = \tLam{h}{P}{h},

である。tt の頭部は引数のない λ\lambda なのに対し、Normalize\Normalize は注釈 Id  P\mathit{Id}\;P の中まで進むからだ。同様に Whnf(Id  (Id  P→P))=Id  P→P\Whnf(\mathit{Id}\;(\mathit{Id}\;P \to P)) = \mathit{Id}\;P \to P であり、定義域はまだ正規形でないものの、すでに Π\Pi の形が見えている。どちらも下で確認でき、頭部に適用できるステップはないのに、正規形は異なる。

Whnf\Whnf の三つの場合には次の性質がある。

  • ι の場合は Nat\Nat、Eq\Eq{}、Tree\mathsf{Tree} への言及を一切含まず、大前提の位置と構成子のデータをシグネチャから読む。これは一度書けば終わりで、以後、新しい帰納型が加えるのはデータの行であってコードではない。
  • 大前提は、計算の後で初めて構成子になることがあるので、規則を参照する前に弱頭正規形にする。第 7 章の 2+12 + 1 では、大前提は数字 1=succ  zero1 = \suc\;\zero で、すでに構成子であるが、double  (1+1)\mathit{double}\;(1 + 1) では大前提は 1+11 + 1 であり、Whnf\Whnf がまずこれを succ  (1+0)\suc\;(1 + 0) にする。
  • 大前提が構成子にならなければ、たとえば変数であれば、項は行き詰まり(stuck)、そのままの形にとどまる。0+n0 + n が簡約されないのはこのためである(第 9 章)。

double\mathit{double} を第 7 章のとおりとして、double  (1+1)\mathit{double}\;(1 + 1) に対する Whnf\Whnf の動作をその場で計算した結果を示す。簡約されるのは常に頭部だけだが、大前提の簡約が内側の計算を強いる。

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
def double : Nat -> Nat := fun (n : Nat) => Nat.rec1 (fun (_n : Nat) => Nat) Nat.zero (fun (k : Nat) => fun (r : Nat) => Nat.succ (Nat.succ r)) n

whnf と記されたステップが内側の計算であり、再帰子は大前提が構成子であることを要求するので、まず 1+11 + 1 を頭部で簡約する。最終的な弱頭正規形には、行き詰まった内側の再帰子がまだ残っており、その下に示す正規形は、Normalize\Normalize が内側へ進み続けて得るものである。

本書の IsDefEq\IsDefEq は、実装を単純にするため両辺を完全に正規化するが、実用的なカーネルは代わりに、両辺を弱頭正規形にして頭部を比較し、必要なときだけ引数を調べる。この遅延比較なら、頭部がすでに異なる大きな項を最後まで正規化せずに済む。型の付く項に対して三つの関数がすべて停止することは、強正規化によって保証される。

コードでは

normalize_with_fuel には触れておくべき細かい点が一つある。引数の正規化が頭部に新しい簡約基を露わにすることがあるので、適用の関数部と引数を正規化した後、組み立て直した項に対してもう一度 whnf を呼ぶ。行き詰まった再帰子の適用で、大前提が自身の引数を正規化して初めて構成子になる場合、頭部をもう一度簡約しなければならない。この再検査がなければ is_def_eq はいくつかの等しさを見逃し、src/proof.rs のテスト conversion_renormalizes_exposed_redex がこれを守っている。

演習

(λ(f:Nat→Nat). f)  (λ(x:Nat). x)  zero(\tLam{f}{\Nat \to \Nat}{f})\;(\tLam{x}{\Nat}{x})\;\zero の弱頭正規形と正規形を求めよ。

解答

背骨の頭部は λ(f:Nat→Nat). f\tLam{f}{\Nat \to \Nat}{f} で、引数は二つある。最初の引数で β 簡約すると (λ(x:Nat). x)  zero(\tLam{x}{\Nat}{x})\;\zero になり、これは再び頭部で β 簡約基なので zero\zero に簡約される。したがって弱頭正規形も正規形も zero\zero である。(これはテスト conversion_renormalizes_exposed_redex の項である。)

Whnf\Whnf はなぜ λ\lambda の下で簡約しないのか。Whnf\Whnf が bb を正規化した λ(x:A). b\tLam{x}{A}{b} を返したとしたら、Infer\Infer で何がまずくなるか。

解答

まずいことは何も起こらず、結果は依然として入力と変換可能であるが、無駄な仕事になる。Infer\Infer が Whnf\Whnf を呼ぶのは最も外側の形を知るためだけであり、頭部が λ\lambda になった時点でそれはもう見えている。内側の簡約は、たとえば何とも比較されない注釈の中など、いくらでも高くつきうる。Whnf\Whnf を浅く保つことで、大きな項に対する型推論が安く保たれる。

覚えておくこと

  • 三つの簡約ステップ。β(λ\lambda を適用する)、δ(定義を展開する)、ι(構成子に対して再帰子を走らせる)。変換は、これらと α 同値が生成する同値関係である。
  • 変換は、両辺を正規化して α 同値まで比較することで判定する。合流性によりこれは正しく、強正規化により停止する。
  • ι 規則はデータ、つまり大前提の位置と構成子のメタデータであり、一つの whnf がすべての帰納型に対して働く。
  • Whnf は頭部でのみ簡約し、Infer が必要とするのはこれである一方、Normalize はあらゆる場所へ進み、IsDefEq が使うのはこれである。ステップの予算がバグに備える。