ここまでに扱った型は宇宙と Π 型だけであり、命題論理には足りても、数学に必要な数や等しさを表す手段はまだない。Lean はその両方を帰納型で提供する。本章ではカーネルに定数を加えて自然数を宣言し、同じ仕組みで数の計算と証明を扱えることを示す。
定数と定義
まず、名前のついた対象をカーネルに追加する手段が要るので、項に定数(constant)c を加える。
t,A,B::=⋯∣c
シグネチャ(signature)Σ は定数を列挙する。各項目は、定数をその型とともに宣言する(c:C と書く)か、さらに本体も与える(c:C:=t と書く)かのどちらかである。本体をもつ項目を定義(definition)と呼ぶ。たとえば、下の Nat:Sort1 は宣言で、本章の後半に出てくる add は定義である。項目を追加する前に、その型が空の文脈で型であることを検査し、本体があればそれをその型に対して検査する。Lean が各 def に対して行うのと同じである。
シグネチャは二通りに使われる。第一に、定数はシグネチャが与える型をもつ。
Γ⊢c:C⊢Γ;ok(c:C)∈Σ;(T-Const)
ここで (c:C)∈Σ は宣言と定義の両方を含む。第二に、定義は展開(unfold)でき、定数をその本体で置き換えるこの計算ステップを δ 簡約(delta-reduction)と呼ぶ。
c→δtif (c:C:=t)∈Σ.
たとえば add21→δ(λ(m:Nat).λ(n:Nat).⋯)21 であり、その後は β 簡約が続く。Nat や zero のような本体のない宣言された定数は、決して δ 簡約されない。β と同様に、このステップはどの部分項にも適用できる。
定数は変数ではなく、決して束縛されないので、FV(c)=∅ であり、代入は定数を変えない。簡約はここから β、δ、そして以下で導入する ι 規則からなり、定義的等価(第 12 章)はこの三つすべてに関して定められる。
自然数
Lean では、自然数は次のように宣言される。
inductive Nat where
| zero : Nat
| succ (n : Nat) : Nat
この宣言によれば、自然数は zero か、ある自然数 n の後続数 succn として作られる。たとえば 2 は succ(succzero) である。私たちのカーネルでは、この宣言によって本体のない三つの定数が追加される。
Nat:Sort1,zero:Nat,succ:Nat→Nat.
zero と succ を構成子(constructor)と呼ぶ。構成子は値を作るが決して計算しないので、succ(succzero) はすでに正規形であり、それ自体が数 2 である。以下では zero、succzero、succ(succzero) などを 0,1,2,… と書く。
これで数は手に入ったが、まだ計算には使えない。加法には zero と succn を区別する手段が必要なのに、今の項は引数を受け渡すだけで、その中身を調べられないからである。
計算: 加法
加法はどう定義すべきか。第二引数に関する再帰で定義する。
addmzero=m,addm(succk)=succ(addmk).
この定義の形に注目してほしい。m を固定すると、関数 addm は二つのデータ、すなわち zero での値(これは m)と、k での値 r を succk での値に変える方法(これは succr)で決まる。この種の再帰を構造的再帰(structural recursion)と呼び、構造的再帰で定義される関数 f:Nat→C はどれも同じ形をしている。
fzero=z,f(succk)=sk(fk),
ここで z:C、s:Nat→C→C である。ステップ関数 s は、k と再帰呼び出しの結果 fk の両方を受け取る。そこで、z と s から f を組み立てる定数を一つ、一度だけ追加する。
recC:C→(Nat→C→C)→Nat→C,
これに二つの計算規則を添える。ι 規則(iota-rule)と呼ばれるもので、上の等式そのものである。
recCzszero→ιz,recCzs(succn)→ιsn(recCzsn).
加法はこれで、再帰を含まないふつうの定義になる。
add:Nat→Nat→Nat:=λ(m:Nat).λ(n:Nat).recNatmsn,s=λ(k:Nat).λ(r:Nat).succr.
addmn を m+n と書くと、カーネルは 2+1 を δ、β、ι のステップだけで計算する。下のエクスプローラは、add の定義を読み込んだカーネルが、頭部ステップを一度に一つずつ実行しているところである。
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
各行が一つの頭部ステップであり、強調された部分が簡約基(redex)で、次の行にその結果が示される。数字を変えてみるか、文脈 n : Nat で add Nat.zero n を試して、再帰子が行き詰まるところを見てほしい。
ほかの関数も同じパターンに従う。double(succk)=succ(succ(doublek)) となる二倍は、
double:=λ(n:Nat).recNatzero(λ(k:Nat).λ(r:Nat).succ(succr))n,
であり、double2→succ(succ(double1))→succ4(double0)→succ4zero=4 となる。
帰納型の定数と、recC およびその二つの規則だけで Nat 上の構造的再帰関数はすべて表せるので、新しい再帰の仕組みは要らない。しかも一般の再帰と違って、各 ι ステップが引数から succ を一つ取り除くので、この再帰は必ず停止する。
証明: 帰納法
次に証明を考える。P:Nat→Sort0 について、性質 Pn がすべての n で成り立つことを示すには、数学では帰納法を使い、基底ケース(base case)Pzero と、帰納ステップ(step)Π(k:Nat).Pk→P(succk) を証明する。帰納ステップの引数 Pk が帰納法の仮定(induction hypothesis)である。
Curry-Howard を通して見ると、Π(n:Nat).Pn の証明とは、n を受け取って Pn の証明を返す関数 f である。base:Pzero と step から、そのような f をどう作るか。3 に対する証明は step2(step1(step0base)) であり、一般には
fzero=base,f(succk)=stepk(fk).
となる。これもまた構造的再帰関数であり、上の add とまったく同じ形をしており、z=base、s=step である。帰納法とは、証明を返す再帰である。
違いは結果の型にあり、add ではどの n に対しても同じ型 C だったが、証明では fzero:Pzero、f1:P1 のように n に依存する。recC の型には固定された C が現れるため、このままでは帰納法を表せない。
両方をこなす一つの再帰子
二つの場合を並べてみると、何を一般化すべきかが見えてくる。
|
再帰 |
帰納法 |
| n に対する結果 |
C |
Pn |
| zero の場合 |
z:C |
base:Pzero |
| succ の場合 |
s:Π(k:Nat).C→C |
step:Π(k:Nat).Pk→P(succk) |
| 結果 |
Nat→C |
Π(n:Nat).Pn |
左の列は、右の列で Pn がどの n に対しても同じ型 C になる特殊な場合である。そこで P を、各 n に対する結果の型を与える任意の関数 M:Nat→Sortu に置き換える。この関数を motive と呼び、そこから図 7.1 の再帰子(recursor)Nat.recu が得られる。再帰子は、motive、構成子ごとに一つの小前提(minor premise)、最後に再帰の対象となる数 n を受け取り、この最後の引数が大前提(major premise)である。ι 規則は recC のものと同じである。Lean の再帰子 Nat.rec.{u} と同様、宇宙レベルごとに一つずつ用意するが、本書の体系には宇宙多相がないため、レベルは添字で表す。
図 7.1. 自然数の定数と、再帰子の ι 規則。
NatzerosuccNat.recu::::Sort1NatNat→NatΠ(M:Nat→Sortu).Mzero→(Π(n:Nat).Mn→M(succn))→Π(n:Nat).MnLean: NatNat.zeroNat.succNat.rec
Nat.recuMzszeroNat.recuMzs(succn)→ι→ιzsn(Nat.recuMzsn)
この一つの再帰子が、motive に応じて両方の仕事をこなす。
再帰。 レベル 1 の定数 motive λ(_:Nat).C を与えると、Nat.rec1(λ(_:Nat).C) の型は recC の型と変換可能である。つまり recC は Nat.rec1(λ(_:Nat).C) にほかならず、以後は
add:=λ(m:Nat).λ(n:Nat).Nat.rec1(λ(_:Nat).Nat)m(λ(k:Nat).λ(r:Nat).succr)n,
とする。これが examples/zero-add.leanmini にある定義である。
帰納法。 レベル 0 の述語 M:Nat→Sort0 を与えると、Nat.rec0 の型は帰納法の原理
Mzero→(∀n.Mn→M(succn))→∀n.Mn.
になる。ここで T-Pi に imax を選んだことが効いてくる。結論 Π(n:Nat).Mn の型は Sortimax(1,0)=Sort0 なので、Nat が Sort1 に属していても、「すべての n について Mn」は命題である。代わりに max を使えばこれは Sort1 に属し、数についての主張はまったく命題にならない。
Nat.recu の型は恣意的なものではなく、Nat の宣言から読み取れる。小前提は構成子ごとに一つあり、各小前提は、構成子の引数を受け取り、さらに型が Nat 自身である引数(再帰的(recursive)引数)ごとに帰納法の仮定を一つ受け取って、構成子に motive を適用したものを返す。zero には引数がないので、その場合はただの Mzero であり、succ には再帰的引数 n が一つあるので、その場合は Π(n:Nat).Mn→M(succn) になる。第 8 章 では、この手順をより一般的な宣言へと順に適用していく。
試してみる
カーネルは、Nat とその再帰子をあらかじめ宣言した状態で始まる(src/kernel.rs の Kernel::standard)。下のスクリプトは加法を定義し、2+1 と 3 が定義的に等しいことをカーネルに確かめさせ、rfl は両辺を計算してこれを行う。three を Nat.succ three に変えて、カーネルが拒否するのを見てほしい。
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 one : Nat := Nat.succ Nat.zero
def two : Nat := Nat.succ one
def three : Nat := Nat.succ two
# 両辺とも succ (succ (succ zero)) に計算されるので、refl でこれを証明できる。
goal Eq Nat (add two one) three
rfl
qed
ゴールの Eq は等式型であり、第 9 章 で宣言する。今のところは、Eq Nat a b を「a=b」、rfl を「両辺は同じものに計算される」と読んでおけばよい。
演習
上の add の流儀で、add を使って、乗法 mulmn を n に関する再帰で定義せよ。
解答
mulmzero=zero かつ mulm(succk)=addm(mulmk) なので、
mul:=λ(m:Nat).λ(n:Nat).Nat.rec1(λ(_:Nat).Nat)zero(λ(k:Nat).λ(r:Nat).addmr)n となる。
def mul : Nat -> Nat -> Nat := fun (m : Nat) => fun (n : Nat) => Nat.rec1 (fun (_n : Nat) => Nat) Nat.zero (fun (k : Nat) => fun (r : Nat) => add m r) n
規則 fixg→g(fixg) をもつ一般の不動点コンビネータ fix:(C→C)→C があれば、どんな再帰関数も定義できる。カーネルがこれを受け入れられないのはなぜか。
解答
C=Π(P:Sort0).P、g=λ(x:C).x とすると、項 fixg は型 Π(P:Sort0).P をもつことになり、これはあらゆる命題の証明であって、体系は整合性を失う。さらにこの項は自分自身へ際限なく簡約されるので、IsDefEq が停止しない。構造的再帰は、各 ι ステップが構成子を一つ消費するので、どちらの問題も起こさない。
覚えておくこと
- 定数はシグネチャ Σ に由来し、定義は δ 簡約で展開され、宣言は決して展開されない。
- 帰納型は型構成子と構成子を追加し、構成子は値を作るが計算はしない。
- Nat 上の構造的再帰は、再帰子という一つの定数に集約され、ι 規則は構成子ごとに一つである。
- 帰納法は、依存的な motive M:Nat→Sort0 を与えた同じ再帰子である。再帰と帰納法は一つの操作である。
- imax の規則があるからこそ、「すべての n について Mn」は命題になる。