Solutions · Part 1

第1部 形式化の基礎

第1–5章 · 15問

第1章

型・項・定義・計算

問題本文
問題1型判断の構造を復元する

Problem

問題

章本文の位置で見る

Nat : Typetwo : Nat を、単なるコロン記法の例ではなく、異なる二つの型判断として分解してください。 式、項、型、判断の四語をそれぞれ定義してください。体系の中で分類される対象を指す語と、 規則の成立について述べる文を指す語を明記します。

続いて Γe:A\Gamma \vdash e : A に、仮定 n : Nat の下で調べる式 n + 1 を代入し、文脈 Γ\Gamma を完全に 書いてください。解答は、判断の左右を項同士の等式と誤解せず、何が仮定で何が結論かを説明できれば完了です。

最後に、本章の加算規則を使って 2+3:Nat2+3 : \mathsf{Nat} の導出木を何も見ずに再構成します。葉、横線、 結論がそれぞれ何を表すかを説明し、右辺を true に替えると、どの前提を作れなくなるかを指してください。

ヒント

コロンの左右だけでなく、判断を成立させる文脈を明示します。加算規則の二つの前提が、同じ型を要求することに 注目してください。

解答

式は形式言語の構文に従う記号列、型は式の使用法を分類する式、項はある型に分類された式です。判断は 「その分類が規則から成立する」という体系上の主張です。Nat : Type は型を宇宙へ、two : Nat は項を 自然数型へ分類します。文脈を Γ=(n:Nat)\Gamma=(n:\mathsf{Nat}) とすると、変数規則と数値規則から Γn:Nat\Gamma\vdash n:\mathsf{Nat}Γ1:Nat\Gamma\vdash1:\mathsf{Nat} を得ます。加算規則により Γn+1:Nat\Gamma\vdash n+1:\mathsf{Nat} です。導出木として書けば

Γn:NatΓ1:NatΓn+1:Nat\frac{\Gamma\vdash n:\mathsf{Nat}\qquad\Gamma\vdash1:\mathsf{Nat}} {\Gamma\vdash n+1:\mathsf{Nat}}

となります。横線の上は規則の前提、下はその前提から得る結論です。true へ替えると、第二前提 Γtrue:Nat\Gamma\vdash\mathsf{true}:\mathsf{Nat} を作れません。

新しい構文を読むときも、構文、形成規則、導入規則、除去規則を分けます。エラーは「値が変」ではなく、 導出木のどの前提が欠けたかとして診断できます。

問題2型検査と評価を別の実験として設計する

Problem

問題

章本文の位置で見る

triple (n : Nat) : Nat を定義し、#check triple 4#eval triple 4 の出力をLeanに渡す前に書いてください。 それぞれのコマンドが検査する入力、返す結果、答えない問いを三列の表にします。さらに 2 : Nat3 : Nat を用い、型が一致しても値の同一性は従わないことを示します。

最後に、型検査に成功するが計算結果をこの章の知識だけでは予測しにくい式と、型検査の前に拒否される式を一つずつ作ります。 両者の差を「検査」と「評価」の語だけに頼らず、入出力を指して説明してください。

ヒント

型検査の出力は型、評価の出力は値です。両方が同じ式を入力にしても、答える問いは異なります。

解答

def triple (n : Nat) : Nat := n + n + n と定義します。#check triple 4 の結果は triple 4 : Nat#eval triple 4 の結果は 12 です。前者は式を自然数として使用できることを保証しますが、 値が12であるとは述べません。後者は閉じた式を計算しますが、全入力についての定理を証明しません。 2 : Nat3 : Nat は同じ型を持ちますが、2 = 3 は偽です。

Lean検査済みL503–508
def triple (n : Nat) : Nat := n + n + n

#check triple 4
#eval triple 4

example : triple 4 = 12 := rfl
出力
triple 4 : Nat
出力
12
補足

抽象的な関数では型検査できても値を表示できない場合があります。その場合も、具体例の評価と一般定理の証明を 混同せず、必要な保証に対応するコマンドを選びます。

問題3拒否される定義を判断の不成立として診断する

Problem

問題

章本文の位置で見る

def bad : Nat := true について、期待型、本体から推論される型、成立させたい判断を順に書いてください。 その上で、宣言型を Bool に変える修正と、本体を自然数に変える修正が、どの判断を成立させるかを比較します。

診断にはLeanのエラー文を貼るだけでなく、各情報を型付け判断へ翻訳した説明を添えます。 修正後の二定義が型検査を通り、元の不成立との差を一つの対応表で示せれば完了です。

ヒント

宣言が要求する型と、本体から推論される型を別々に書きます。

解答

def bad : Nat := true の期待型は Nat、本体 true の推論型は Bool です。成立させたい判断は true : Nat ですが、その導出規則はありません。def goodBool : Bool := true は期待型を本体へ合わせます。 def goodNat : Nat := 0 は本体を期待型へ合わせます。どちらも宣言全体の型整合性を回復します。

関数本体のエラーでも、期待される出力型と各分岐の推論型を表にすると同じ診断ができます。最初に現れた エラー文だけでなく、要求された判断まで戻ることが重要です。

Lean検査済みL514–518
def goodBool : Bool := true
def goodNat : Nat := 1

#check goodBool
#check goodNat
出力
FormalLab.Appendix.Solutions.Chapter001Exercise003.goodBool : Bool
出力
FormalLab.Appendix.Solutions.Chapter001Exercise003.goodNat : Nat

第2章

関数・適用・合成

問題本文
問題1合成の向きを型だけから復元する

Problem

問題

章本文の位置で見る

f : A → Bg : B → C だけを与え、compose g f の本体を見ずにラムダ項を導いてください。 始域、中間型、終域を図に書き、入力 x : A がどの順で移るかを各適用の型判断とともに示します。 次に compose f g を同じ方法で診断し、どの二型が一致すれば例外的に型検査できるかも述べます。

数学的記法 A \xrightarrow{f} B \xrightarrow{g} C、ラムダ式 λx. g (f x)、Leanの compose g f を三行に並べ、各部分の対応を示してください。 解答は記法の暗記ではなく、中間型 B が合成の順序を一意に強制することを説明できれば完了です。

ヒント

合成の中央の型が一致するように、右側の関数から入力を流します。

解答

f:ABf:A\to Bg:BCg:B\to C から作れる合成は gf:ACg\circ f:A\to C です。入力 a:Aa:A をまず ff へ渡して f(a):Bf(a):B を得て、それを gg へ渡します。逆向きの fgf\circ g には、一般には gg の出力 Cff の入力 A として使う根拠がありません。Leanの compose g f x := g (f x) も同じ順序です。

関手の合成や自然変換の垂直合成でも、中央の始域・終域を照合すれば向きを復元できます。記号の読み順より 先に型を並べます。

問題2カリー化と部分適用を実装と判断で区別する

Problem

問題

章本文の位置で見る

multiply : Nat → Nat → Nat を定義し、multiply 2 の型を予想してから doubleByMultiplication : Nat → Nat として名前を与えます。二引数関数を一引数関数の反復で表すことと、 その関数に実際に一引数を与えることを、定義時と使用時の異なる操作として説明してください。

さらに暗黙引数を一つ持つ関数を作ります。カリー化、部分適用、暗黙引数の三者を比較表にします。 表には、構文上の引数、省略される引数、新しい関数値が生じる時点を記入してください。

ヒント

A → B → C を右結合で読み、一引数を与えた後に残る型を書きます。

解答

カリー化された関数の型は A(BC)A\to(B\to C) です。multiply : Nat → Nat → Nat2 を与えると、 multiply 2 : Nat → Nat という新しい関数が残ります。これが部分適用です。カリー化は多引数関数を一引数関数の 連鎖として表す変換、部分適用はその表現へ実際に一部の引数を与える操作です。

定理の暗黙引数や型クラス引数も、適用後に残る関数型を見れば同じ方法で追えます。どの引数が既に固定され、 どれがまだ量化されているかを区別します。

Lean検査済みL524–530
def multiply : Nat → Nat → Nat := fun left right => left * right
def doubleByMultiplication : Nat → Nat := multiply 2
def singleton {α : Type} (x : α) : List α := [x]

#check multiply 2
#eval doubleByMultiplication 7
#check singleton true
出力
multiply 2 : Nat → Nat
出力
14
出力
singleton true : List Bool
問題3合成の単位則を等式による証明とLean証明で対照する

Problem

問題

章本文の位置で見る

任意の f : A → B について、id ∘ f = ff ∘ id = f をまず等式列で証明します。関数の等しさを 点ごとの等しさへ帰着させ、任意の入力で両辺を計算してください。次に同じ証明をLeanで書き、 funext が担う段階と rfl で閉じる計算段階を分けて注釈します。

検証として、関数外延性を使わずに各点の等式だけを証明した場合、なぜ元の関数等式がまだ得られていないかを述べてください。

ヒント

関数等式を直接示す代わりに、任意の入力へ適用した値を比較します。

解答

任意の f:ABf:A\to Bx:Ax:A について、(idBf)(x)=idB(f(x))=f(x)(\mathrm{id}_B\circ f)(x)=\mathrm{id}_B(f(x))=f(x) です。 同様に (fidA)(x)=f(idA(x))=f(x)(f\circ\mathrm{id}_A)(x)=f(\mathrm{id}_A(x))=f(x) です。点ごとの等式から関数外延性により idBf=f\mathrm{id}_B\circ f=ffidA=ff\circ\mathrm{id}_A=f が従います。

結合則も三つの関数を任意の入力へ適用し、両辺が h (g (f x)) へ簡約されることを示せます。 圏論ではこの点ごとの証明が射の単位則・結合則の原型になります。

Lean検査済みL536–548
def identity {α : Type} (x : α) : α := x
def compose {α β γ : Type} (g : β → γ) (f : α → β) : α → γ :=
  fun x => g (f x)

theorem leftIdentity {α β : Type} (f : α → β) :
    compose identity f = f := by
  funext x
  rfl

theorem rightIdentity {α β : Type} (f : α → β) :
    compose f identity = f := by
  funext x
  rfl

第3章

帰納型・場合分け・再帰・構造体

問題本文
問題1四種類の規則から有限データ型を設計する

Problem

問題

章本文の位置で見る

三構成子 redyellowgreen を持つ TrafficLight を設計します。Leanコードを書く前に、形成規則、 三つの導入規則、全分岐による除去規則、各分岐の計算規則を数式で書いてください。その後で次の色を返す next を実装し、三分岐が入力を尽くすことを構成子一覧から説明します。

SwitchTrafficLight の除去原理を比較し、構成子数が増えたときに何が変わり、何が同じ図式のままかを述べます。 検証は next red = greennext green = yellownext yellow = red を計算で確かめるだけでなく、それぞれがどの計算規則の実例かを指します。

ヒント

値を作る規則、全ての値を使う規則、値を計算へ渡す規則、構成子上の計算規則を分けます。

解答

形成規則は TrafficLight:Type\mathsf{TrafficLight}:\mathsf{Type}、導入規則は red,yellow,green:TrafficLight\mathsf{red},\mathsf{yellow},\mathsf{green}:\mathsf{TrafficLight} です。任意の型 CCcr,cy,cg:Cc_r,c_y,c_g:C から、全ての t:TrafficLightt:\mathsf{TrafficLight} に対する値を作れるのが除去規則です。 各構成子へ適用した結果が対応する分岐へ簡約される三式が計算規則になります。

列挙型の構成子を増やす場合も、各構成子に一つの分岐と計算規則を対応させます。再帰的な構成子を加えると、 除去規則には再帰結果または帰納仮定も現れます。

Lean検査済みL554–567
inductive TrafficLight where
  | red
  | yellow
  | green
  deriving DecidableEq

def next : TrafficLight → TrafficLight
  | .red => .green
  | .green => .yellow
  | .yellow => .red

example : next .red = .green := rfl
example : next .green = .yellow := rfl
example : next .yellow = .red := rfl
問題2場合分けと構造再帰を呼出し関係で見分ける

Problem

問題

章本文の位置で見る

togglelistLength の定義を、分岐ごとに「構成子の引数」「再帰呼出し」「返り値」の三列で書き直してください。 listLength の再帰呼出しが元の入力ではなく直接の部分構造に対して行われることを、引数の構文から説明します。

次に、同じ入力をそのまま再帰呼出しする不正な長さ関数を試し、Leanの停止性診断を記録します。 エラー文の引用で終えず、どの再帰呼出しに対して構造的な減少を示せないかを入力木に印をつけて示してください。

ヒント

右辺が元の関数を小さい部分構造へ再び呼び出しているかを調べます。

解答

toggle は入力の最上位構成子だけを見て値を返すため場合分けです。factorial (n+1)factorial n を使い、listLength (x :: xs)listLength xs を使うため構造再帰です。 どちらも構成子ごとに分岐しますが、再帰では構成子の引数に含まれる小さいデータへ呼出し辺があります。

木の高さや式の評価器でも、各再帰呼出しが直下の部分木へ向くかを確認します。単に match があるかではなく、 呼出し関係で分類します。

Lean検査済みL573–577
def listLength {α : Type} : List α → Nat
  | [] => 0
  | _ :: tail => listLength tail + 1

#eval listLength [10, 20, 30]
出力
3
問題3帰納型の無限と余帰納的観察を混同しない

Problem

問題

章本文の位置で見る

Nat.rec の型を転写し、基底値、帰納段階、入力自然数、出力の四成分を色分けします。次にそれらを factorial の零分岐、後継者分岐、再帰結果、最終出力へ対応させ、再帰定義が除去原理の使用であることを説明してください。

自然数型が無限個の値を持つ主張と、任意の一つの自然数が有限回の succ で生成される主張を、量化子の順序を含む二文で書き分けます。 その上で、無限ストリームに求められる「任意の有限段階まで観察できる」という性質が、有限な構成子列とどう異なるかを論じます。

ヒント

「値の個数が無限」と「一つの値を無限に観察できる」を別の量化として書きます。

解答

自然数型は有限個の構成子 zerosucc から生成されますが、succ を任意の有限回使えるため値の集合は 無限です。ただし各自然数値は有限の構成木です。ストリームは一つの値から先頭と次の状態を何度でも観察でき、 有限構成木として全体を展開し終えません。前者は基数の無限、後者は観察の非終端性です。

無限木や状態機械でも、状態数、生成される有限値の個数、一つの振舞いの観察長を別々に記述します。

第4章

非型付きラムダ計算——束縛・代入・簡約

問題本文
問題1名前付き項とde Bruijn添字を往復する

Problem

問題

章本文の位置で見る

λ x. λ y. x yλ a. λ b. a b をde Bruijn添字へ翻訳し、両者が同じ構文になることを確かめてください。 各添字から指される束縛子へ線を引き、束縛子が一つ増えるごとに同じ変数の番号がどう変化するかを説明します。

次に、単なるα変換、束縛を無視した不正な文字列置換、捕獲回避代入の三例を作ります。各例で自由変数集合を代入の前後に計算し、 何が保存されなければならないかを判定基準にしてください。

ヒント

変数名ではなく、その変数を束縛するラムダまで何個の束縛を越えるかを数えます。

解答

λx.x\lambda x.xlam (var 0)λx.λy.x\lambda x.\lambda y.xlam (lam (var 1)) です。内側で y は 直近の束縛なので0、x は一つ外側なので1です。逆変換では束縛子へ新しい名前を割り当て、添字0から外側へ 環境を参照します。自由変数は現在の束縛深さ以上の添字として別に管理します。

型変数と項変数を同時に持つ体系では、二種類の束縛深さを分けます。名前を消す目的は捕獲をなくすことではなく、 捕獲回避を添字操作へ変換することです。

問題2シフトと代入の不変条件を一段ずつ追う

Problem

問題

章本文の位置で見る

substitute 0 (.var 2) (.lam (.app (.var 1) (.var 0))) を再帰呼出しごとに展開し、現在の深さ、置換対象の添字、 shiftAbovecutoff、得られる部分項を表に記録してください。cutoff 未満の添字が現在の束縛子に属するため動かしてはならず、 cutoff 以上だけが自由変数または外側の束縛を指すことを、具体的な項の線図で示します。

対照として、このシフトを省略した素朴な代入を一つ書き、自由だった添字が新しいラムダに捕獲される最小の反例を作ってください。

ヒント

ラムダの下へ入るとき、cutoffと代入対象の添字がともに一つ増える理由を追います。

解答

自由変数を持つ項をラムダの下へ移す前に、cutoff以上の添字を1増やします。代入 [j:=s]t[j:=s]t でラムダの下へ入る場合は、対象を j+1j+1 とし、置換項を shift s に替えます。 これにより、外側で自由だった変数が新しい束縛子の var 0 に誤って捕獲されません。束縛変数はcutoff未満なので シフトされず、同じ束縛先を保ちます。

型代入や文脈への弱化でも、保ちたい不変条件を「各添字が同じ束縛を指す」と書けば、必要なシフト量を導けます。

Lean検査済みL583–596
open FormalLab.Foundation.UntypedLambdaCalculus

def naiveSubstitute (index : Nat) (replacement : Term) : Term → Term
  | .var found => if found = index then replacement else .var found
  | .app function argument =>
      .app (naiveSubstitute index replacement function)
        (naiveSubstitute index replacement argument)
  | .lam body => .lam (naiveSubstitute (index + 1) replacement body)

example :
    naiveSubstitute 0 (.var 0) (.lam (.var 1)) = .lam (.var 0) := rfl

example :
    substitute 0 (.var 0) (.lam (.var 1)) = .lam (.var 1) := rfl
問題3簡約規則から評価器の探索方針を分離する

Problem

問題

章本文の位置で見る

β規則、一段簡約関係、評価戦略、β正規形をそれぞれ定義し、一つの項が複数のredexを持つ例で差を示してください。 直下の関数位置、その後に引数位置を一段だけ探索する簡約関数を実装し、探索順を逆にした版と結果を比較します。

[DB72] の冒頭で無名記法が解決しようとする問題を確認し、原論文の記法と本章の TermshiftAbovesubstitute の対応を説明します。 最後に、この Term がLean内に定義した対象言語のデータであり、Lean自身の項と同一ではない理由を説明してください。 Leanが関数 contractHead? を評価することと、Step がラムダ項について表す一段簡約とを、それぞれの入力・出力を挙げて比較します。

ヒント

どの局所変形が許されるかと、複数の候補からどれを選ぶかを分けます。

解答

β簡約規則は (λx.t)st[x:=s](\lambda x.t)\,s\to t[x:=s] という局所変形を許します。これは項のどこを先に探すかを 指定しません。contractHead? は先頭がredexのときだけ縮約する関数です。完全な値呼び評価器なら関数位置、 次に引数を値まで評価し、その後にβ簡約します。名前呼びなら引数を先に評価せず代入します。

書換え系、証明簡約、最適化でも、関係を仕様、探索戦略を実装として分離します。完全性や決定性は別の定理です。

Lean検査済みL602–627
open FormalLab.Foundation.UntypedLambdaCalculus

def functionFirst? : Term → Option Term
  | .app (.lam body) argument => some (substitute 0 argument body)
  | .app function argument =>
      match contractHead? function with
      | some function' => some (.app function' argument)
      | none => (contractHead? argument).map (.app function)
  | _ => none

def argumentFirst? : Term → Option Term
  | .app (.lam body) argument => some (substitute 0 argument body)
  | .app function argument =>
      match contractHead? argument with
      | some argument' => some (.app function argument')
      | none => (contractHead? function).map (fun function' => .app function' argument)
  | _ => none

def twoRedexes : Term :=
  .app (.app identity constant) (.app identity identity)

example : functionFirst? twoRedexes =
    some (.app constant (.app identity identity)) := rfl

example : argumentFirst? twoRedexes =
    some (.app (.app identity constant) identity) := rfl

第5章

宇宙階層と宇宙多相

問題本文
問題1コロンの連鎖を三つの判断へ戻す

Problem

問題

章本文の位置で見る

0 : Nat : Type : Type 1 という略記を三つの独立した判断へ分解し、各判断で左辺が項なのか型なのかを述べてください。 #check Type#check Type 1#check Type 2 の出力を予想して確かめ、一般の Type u : Type (u + 1) へ引き上げます。

次に、仮に Type : Type を許した場合と、実際の宇宙変数 u を使った場合を対比します。宇宙多相が階層を消すのではなく、 定義ごとに階層番号を一般化することを、一つの具体的な型付け例で示してください。

ヒント

Nat : Type : Type 1 を一つの三項関係として読まず、隣接する二つの判断へ分けます。

解答

第一の判断は Nat : Type で、自然数型を宇宙 Type の項として分類します。第二の判断は Type : Type 1 で、その宇宙自身を一段上の宇宙へ分類します。項 0 : Nat を加えると、 0 : NatNat : TypeType : Type 1 の三判断になります。Type : Type とはしないため、 自己包含に由来する不整合を避けます。

依存型の型を読むときも、項、型、型の属するsortを別の判断へ展開します。エラーが値の型か宇宙制約かを 切り分けられます。

Lean検査済みL633–641
#check Type
#check Type 1
#check Type 2

universe u

def universePolymorphicIdentity {α : Type u} (x : α) : α := x

#check universePolymorphicIdentity (α := Type) Nat
出力
Type : Type 1
出力
Type 1 : Type 2
出力
Type 2 : Type 3
出力
universePolymorphicIdentity Nat : Type
問題2宇宙変数が一つでは足りない定義を診断する

Problem

問題

章本文の位置で見る

polymorphicCompose の始域、中間型、終域をそれぞれ Type uType vType w に属させます。 そのうえで、三変数が独立である使用例を作ります。 次に三変数を一つに固定した版を書き、その版では表現できない合成を示します。

非宇宙多相な identitypolymorphicIdentityULift について、「定義を再利用する宇宙」「値が実際に属する宇宙」 「型を明示的に持ち上げるか」を比較表にします。三者を単なる「汎用化」の強弱として並べない説明が必要です。

ヒント

二つの入力型が同じ宇宙に属する必要が本当にあるかを考えます。

解答

定数関数や合成は、入力型と出力型が異なる宇宙にあっても構成できます。α : Type uβ : Type v を 一つの u に固定すると、定義に不要な同宇宙制約を課します。polymorphicConstant では uvpolymorphicCompose では u,v,w を独立に量化するのが一般的です。

圏の対象・射や型族でも、独立に選べる層には独立な宇宙変数を置きます。等しさが必要な制約だけを共有します。

Lean検査済みL647–657
universe u v w

def independentCompose {α : Type u} {β : Type v} {γ : Type w}
    (g : β → γ) (f : α → β) : α → γ :=
  fun x => g (f x)

def oneUniverseCompose {α β γ : Type u}
    (g : β → γ) (f : α → β) : α → γ :=
  fun x => g (f x)

#check independentCompose (α := Nat) (β := Type) (γ := Type 1)
出力
independentCompose : (Type → Type 1) → (Nat → Type) → Nat → Type 1
問題3`max` と `imax` を仕様から読み、具体例へ適用する

Problem

問題

章本文の位置で見る

α : Type uβ : Type v に対する Prod α β : Type (max u v) を、u < vu = vv < u の三場合に分けて計算します。 各場合で出力宇宙が両成分を収容できる最小の階層になることを説明してください。

[LEAN-REF] の宇宙節で maximax の定義上の役割を確認し、非依存直積と依存関数型の宇宙計算を比較します。 解答には仕様の該当節を明記し、引用した規則がどのLean型に現れるかを少なくとも一例示します。

ヒント

直積型と依存関数型について、どの成分が Prop のとき宇宙が縮むかを比較します。

解答

α × β は両方のデータを保持するため、α : Type uβ : Type v から α × β : Type (max u v) になります。依存関数型 (x : α) → β x の宇宙は通常 imax u v です。 終域が Prop、すなわち v=0 のとき、全称命題も Prop に留まります。これが imax u 0 = 0 の役割です。

Lean検査済みL663–669
universe u v

def universeProduct (α : Type u) (β : Type v) : Type (max u v) := α × β
def allInUniverse {α : Type u} (p : α → Prop) : Prop := ∀ x, p x

#check @universeProduct
#check @allInUniverse
出力
universeProduct : Type u_1 → Type u_2 → Type (max u_1 u_2)
出力
@allInUniverse : {α : Type u_1} → (α → Prop) → Prop
補足

宇宙式は暗記せず、構成がデータを保持するか、証明だけを要求するかから予測します。予測後にLeanの表示で 制約が解けることを確かめます。