数学系エンドユーザーのためのLean入門¶
講義概要¶
- https://www.math.kyoto-u.ac.jp/ja/event/seminar/6028
- 期間: 2026年6月22日(月)〜26日(金)
- 担当: 園田翔(理化学研究所 / サイバーエージェント)
講義資料¶
- (本編) https://shosonoda.github.io/lean-math-note/
- (GitHub) https://github.com/shosonoda/lean-math-note/
- (環境構築) https://shosonoda.github.io/lean-install/
このプロジェクトで指定する Lean version
leanprover/lean4:v4.30.0
目次¶
- 基礎編
- 実践編
参考文献¶
- Lean 公式 https://lean-lang.org/
- Avigad and Massot, Mathematics in Lean, Jun 11, 2026.
- Avigad et al., Theorem Proving in Lean 4, 2026.
-
The Lean Developers, The Lean Language Reference, v4.30.0., 2026.
-
井上, ゼロから始めるLean言語入門, ラムダノート, 2025
- Lean-by-Example https://lean-ja.github.io/lean-by-example/
- 数学系のためのLean勉強会 https://haruhisa-enomoto.github.io/lean-math-workshop/
このサイトについて¶
-
印刷用ページ (1枚につなげた資料)
-
謝辞: 本資料の改善点を指摘してくださった皆様に感謝します.とりわけ折池雄太さん,水野勇磨さん,井上亜星さん,中川康二さんに感謝します.
Chapter 01: 命題論理と述語論理の形式証明¶
Lean では,命題は Prop という型の項として表されます.
つまり P : Prop は「P は命題である」という意味です.
そして,命題 P の証明は,型 P をもつ項 h : P として扱われます.
このような項を,P の証明,あるいは証明項と呼びます.
この見方は「命題を型,証明を項として見る」考え方で,Curry--Howard 対応あるいは propositions-as-types(命題=型対応)と呼ばれます.
証明項 h : P を作ることと,命題 P を証明することは同じことであり,Lean の型検査器が証明の正しさを保証します.
つまり,Lean における形式証明では「正しい証明であること」を,最終的には「指定された型をもつ項が構成できていること」として確認します.
タクティックモードでは,Lean が現在の「ゴール」を表示し,ユーザは intro,exact,constructor,cases などのタクティックでゴールを変形していきます.
各タクティックは最終的に証明項を作るための補助であり,完成した証明は Lean の小さなカーネルによって再度型検査されます.
以下では,直観主義自然演繹でよく使う推論規則を 1 つずつ確認し,対応する Lean の書き方を見ます.
形式証明の用語¶
形式証明では,「式がどの規則で導かれたか」と「式が何を意味するか」を分けて考えます. この区別は Lean を学ぶ上で重要です.
証明論は,記号列としての命題と,それを変形する推論規則を扱います. この立場での「証明」は,有限個の推論規則を順に適用して結論に到達する構文的な対象です. 自然演繹の証明図でいえば,根に結論をもち,各節点で決められた推論規則だけを使っている木が証明です. Lean の証明項も,この意味で検査可能な構文的対象です.
意味論は,命題を解釈するモデルや構造を与え,その解釈のもとで命題が真か偽かを考えます. たとえば命題論理では,各命題変数に真理値を割り当てたときに式全体の真理値が決まります. この真理値は「意味」の側の概念であり,形式証明そのものではありません. 健全性定理は「証明できる式は意味論的に正しい」と述べ,完全性定理は,対象となる論理に応じて「意味論的に正しい式は証明できる」と述べる定理です.
命題論理や述語論理は,どのような式を作るかを定める論理の言語です.
命題論理では P ∧ Q や P → Q のように命題を論理結合子で組み合わせます.
述語論理ではさらに,対象変数,述語,∀ や ∃ などの量化子を扱います.
一方,自然演繹やシーケント計算は,そのような式をどの規則で証明するかを定める証明体系です.
同じ命題論理や述語論理に対して,自然演繹で証明することも,シーケント計算で証明することもできます.
この章で扱う規則は,主に直観主義自然演繹の導入規則と除去規則です. Gentzen の記法に合わせて,命題論理の直観主義自然演繹を NJ と呼ぶことがあります. 量化子も含めて話すときは,「直観主義一階自然演繹」と呼ぶのが正確です. 後で出てくる排中律や背理法は NJ だけでは証明できないため,古典論理の原理として区別して扱います. それらを自然演繹に追加した体系は,古典自然演繹,あるいは Gentzen の記法では NK と呼ばれます.
なお,この章では \(\Gamma \vdash P\) という形の表記を使いますが,これは「仮定の集まり \(\Gamma\) のもとで \(P\) が証明できる」という判断を表すための記法です. この表記自体はシーケント風ですが,ここで説明している規則はシーケント計算の左規則・右規則ではなく,自然演繹の導入規則・除去規則です. したがって,この章の規則を指すときは「自然演繹の規則」,より具体的には「直観主義自然演繹 NJ の規則」と呼ぶのがよいです.
自動証明では,SAT/SMT solver や theorem prover が,探索によって証明や反例を見つけようとします.
多くの場合,ユーザは問題を入力し,システムができるだけ自動で結論を出します.
一方,Lean のような対話型証明支援系では,ユーザが証明の方針や中間ステップを与え,システムが現在のゴールを表示しながら証明を組み立てます.
Lean にも simp や omega などの自動化されたタクティックはありますが,最終的には生成された証明項をカーネルが型検査する,という点が中心です.
Lean の基礎は,ZFC 集合論ではなく,依存型理論です.
ZFC では基本的にすべての数学的対象を集合として扱い,定理は一階述語論理の式として表されます.
Lean では対象は型をもち,命題は Prop という型の項として扱われ,その命題の証明はその型の項として扱われます.
つまり Lean では,P : Prop に対して h : P という項を構成することが,P を証明することです.
Mathlib の中で集合や位相空間や群を扱うことはできますが,Lean の基礎そのものは「すべてを集合に還元する」立場ではありません.
この「命題を型,証明を項として見る」対応は,型付きラムダ計算と深く結びついています.
含意 P → Q の証明は,P の証明を受け取って Q の証明を返す関数です.
含意導入はラムダ抽象 fun h : P => ... に対応し,含意除去は関数適用に対応します.
連言の証明はペア,連言除去は射影に対応します.
したがって,紙の上の証明木,Lean の tactic proof,Lean が内部で作る証明項は,見た目は違っても同じ証明構造を別の表現で見ていると考えられます.
特に含意や全称命題の証明では,証明項が fun h : P => ... のような関数として現れます.
Lean の命題の読み方¶
この章では,Lean の構文そのものには深入りしません.
ただし,証明例を読むために example と theorem の基本形だけ確認しておきます.
これは,「P と Q を命題とし,hPQ : P → Q と hP : P を仮定すると,Q が証明できる」という意味です.
(P Q : Prop) は「P と Q は命題である」という宣言です.
(hPQ : P → Q) と (hP : P) は,それぞれ「P → Q の証明」と「P の証明」を仮定として受け取る,という意味です.
読み慣れないうちは,カッコで囲まれた部分をいったん飛ばして,カッコの外にある最後の : Q を「ゴールは Q」と読むとよいです.
そのあとで,左から順にカッコ内を「使ってよい変数や仮定」として読み戻します.
カッコの中の : は「左の名前が右の型をもつ」という型注釈で,最後の : Q はこの example 全体が示す命題です.
:= by 以降が証明本体で,by の後では tactic を使ってゴールを解いていきます.
theorem は,証明に名前を付ける点を除けば同じように読めます.
example は名前のない練習用の定理で,後から参照することは通常しません.
theorem modusPonensExample ... と書くと,証明した命題に modusPonensExample という名前が付き,別の場所でその名前を使えるようになります.
この章では主に example を使いますが,読み方は theorem と同じです.
詳しいコマンドや型の読み方は Chapter 02 で扱います.
証明図の読み方¶
証明図では,横線の上に前提,横線の下に結論を書きます. 右側の \((\to I)\) や \((\land E)\) は,使っている推論規則の名前です. ここでの証明図は自然演繹の規則を表示しています. シーケント計算では,\(\Gamma \vdash P\) のようなシーケントそのものを左規則・右規則で変形しますが,この章では論理結合子を導入する規則と,すでにある証明から情報を取り出す除去規則を中心に見ます. たとえば
は,「同じ仮定の集まり \(\Gamma\) のもとで \(P \to Q\) と \(P\) が証明できるなら,\(Q\) が証明できる」と読みます.
ここで \(\Gamma\) は「現在使ってよい仮定や変数の集まり」です.
Lean のコードでは,通常 \(\Gamma\) という記号は直接書きません.
かわりに,example や theorem の引数,intro で導入した仮定,cases で場合分けして得た仮定が,Lean のローカルコンテキストとして管理されます.
VS Code の Infoview では,ゴールの上に並ぶ変数や仮定が,証明図の \(\Gamma\) に対応します.
たとえば
では,Lean のローカルコンテキストにはおおよそP : Prop と hP : P があります.
このうち hP : P は「\(P\) の証明を仮定として使ってよい」という意味で,証明図の \(P \in \Gamma\) に対応します.
述語論理では,x : α のような対象変数もコンテキストに入ります.
証明図では,命題の仮定と対象変数をまとめて \(\Gamma\) と省略している,と考えるとよいです.
intro hP は,含意を示す途中で一時的に hP : P をコンテキストに追加する操作です.
証明図では,これは横線の上側に出てくる \(\Gamma, P \vdash Q\) の \(P\) に対応します.
証明が終わると,その仮定は「もし \(P\) ならば」という含意の中に閉じ込められ,結論は \(\Gamma \vdash P \to Q\) になります.
命題論理¶
命題論理では,個々の命題 P Q R : Prop を対象にして,論理結合子
∧,∨,→,¬,↔,True,False を扱います.
Lean では,これらの論理記号も型として実装されています.
たとえば P → Q は「P の証明を受け取って Q の証明を返す関数型」です.
P ∧ Q は And P Q,P ∨ Q は Or P Q,P ↔ Q は Iff P Q の記法です.
また,¬ P は定義上 P → False の略記です.
したがって,論理結合子の導入規則は「その型の項を作る方法」,除去規則は「その型の項から情報を取り出して使う方法」と読めます.
この視点を持つと,constructor が導入規則,cases や .left,.right が除去規則に対応する理由が見えやすくなります.
仮定規則¶
すでに仮定 hP : P があるなら,結論 P はその仮定そのもので証明できます.
Lean では,仮定名を exact で渡します.
この規則は自然演繹では仮定規則,仮説規則,あるいは単に assumption rule と呼ばれます. 証明論の本では,前提をもたない初期規則という意味で axiom と表示されることもあります. 特にシーケント計算では,対応する規則を identity axiom や initial sequent と呼び,たとえば \(\Gamma, P \vdash P\) の形で書きます. したがって,紙の上の証明体系で「仮定規則を axiom と書く」こと自体はあります.
ただし,ここでの axiom は,Lean の axiom コマンドとは意味が違います.
hP : P は現在の証明の中で使ってよいローカルな仮定であり,exact hP はその仮定として与えられた証明項をそのまま使っています.
一方,Lean で axiom と宣言すると,証明を構成せずに大域的な定数を環境へ追加することになります.
これはローカルな仮定を使うことより強い操作なので,通常の証明中の hP : P とは区別します.
証明図:
具体例: 「\(n\) は偶数である」と仮定しているなら,証明の途中でそのまま「\(n\) は偶数である」と言ってよい,という規則です.
含意導入 → introduction¶
P → Q を証明するには,いったん P を仮定して,そのもとで Q を証明します.
Lean では intro hP により,ゴール P → Q を「仮定 hP : P のもとで Q を示す」というゴールに変えます.
証明図:
具体例: 「\(n\) が偶数なら \(n^2\) も偶数である」を示すとき,まず「\(n\) は偶数である」と仮定し,その仮定のもとで「\(n^2\) は偶数である」を示します.
含意除去 → elimination¶
含意除去は modus ponens です.
hPQ : P → Q と hP : P があれば,hPQ hP : Q が得られます.
証明図:
具体例: 「収束する数列は有界である」と「数列 \((a_n)\) は収束する」が分かっていれば,「\((a_n)\) は有界である」と結論できます.
連言導入 ∧ introduction¶
P ∧ Q を証明するには,P の証明と Q の証明を両方与えます.
Lean では constructor がゴールを P と Q の 2 つに分解します.
証明図:
具体例: 「\(x > 0\)」と「\(y > 0\)」をそれぞれ証明できれば,「\(x > 0\) かつ \(y > 0\)」を証明できます.
連言除去 ∧ elimination¶
h : P ∧ Q があるなら,左成分 h.left : P と右成分 h.right : Q を取り出せます.
証明図:
具体例: 「\(x > 0\) かつ \(y > 0\)」が分かっていれば,左成分として「\(x > 0\)」を取り出せますし,右成分として「\(y > 0\)」も取り出せます.
example (P Q : Prop) (h : P ∧ Q) : P := by
exact h.left
example (P Q : Prop) (h : P ∧ Q) : Q := by
exact h.right
選言導入 ∨ introduction¶
P ∨ Q を証明するには,左側の P を証明するか,右側の Q を証明すれば十分です.
Lean では左を選ぶとき Or.inl,右を選ぶとき Or.inr を使います.
証明図:
具体例: 「\(n = 0\)」が分かっていれば,「\(n = 0\) または \(n > 0\)」を結論できます. 同様に,「\(n > 0\)」が分かっている場合も「\(n = 0\) または \(n > 0\)」を結論できます.
example (P Q : Prop) (hP : P) : P ∨ Q := by
exact Or.inl hP
example (P Q : Prop) (hQ : Q) : P ∨ Q := by
exact Or.inr hQ
選言除去 ∨ elimination¶
h : P ∨ Q から R を示すには,P の場合に R が出ることと,Q の場合に R が出ることを両方示します.
Lean では cases h with により,左の場合と右の場合に場合分けします.
証明図:
具体例: 自然数 \(n\) について「\(n = 0\) または \(n > 0\)」が分かっていて,どちらの場合にも「\(n \ge 0\)」が言えるなら,結論として「\(n \ge 0\)」が言えます.
example (P Q R : Prop) (h : P ∨ Q) (hPR : P → R) (hQR : Q → R) : R := by
cases h with
| inl hP =>
exact hPR hP
| inr hQ =>
exact hQR hQ
真の導入 True introduction¶
True は常に証明できます.
Lean では True.intro が True の標準的な証明です.
証明図:
具体例:
どのような仮定のもとでも,論理的に常に正しい命題 True は証明できます.
数学の議論では,情報を持たない自明な結論を置く場合に対応します.
偽の除去 False elimination¶
False の証明があるなら,任意の命題 P を証明できます.
これは爆発律(ex falso quodlibet)と呼ばれます.
Lean では False.elim hFalse を使います.
証明図:
具体例: もし仮定から「\(0 = 1\)」のような矛盾が導けてしまったなら,その矛盾から任意の命題を導けます. もちろん,通常の一貫した数学では矛盾そのものを導けないように注意します.
否定導入 ¬ introduction¶
Lean では ¬ P は P → False の略記です.
したがって ¬ P を証明するには,P を仮定して矛盾 False を導きます.
証明図:
具体例: 「\(\sqrt{2}\) は有理数である」と仮定すると矛盾が出ることを示せば,「\(\sqrt{2}\) は有理数ではない」と結論できます.
否定除去 ¬ elimination¶
hP : P と hNotP : ¬ P が同時にあると,False が得られます.
さらに False.elim を使えば任意の結論を導けます.
証明図:
具体例:
同じ文脈で「\(x > 0\)」と「\(x > 0\) ではない」が同時に得られたら,矛盾 False が得られます.
example (P : Prop) (hP : P) (hNotP : ¬ P) : False := by
exact hNotP hP
example (P Q : Prop) (hP : P) (hNotP : ¬ P) : Q := by
exact False.elim (hNotP hP)
同値導入 ↔ introduction¶
P ↔ Q を証明するには,P → Q と Q → P の両方向を証明します.
Lean では constructor が 2 つの方向にゴールを分解します.
証明図:
具体例: 「\(P \land Q\) と \(Q \land P\) は同値である」を示すには,\(P \land Q\) から \(Q \land P\) を示す方向と,\(Q \land P\) から \(P \land Q\) を示す方向の両方を証明します.
example (P Q : Prop) (hPQ : P → Q) (hQP : Q → P) : P ↔ Q := by
constructor
· intro hP
exact hPQ hP
· intro hQ
exact hQP hQ
同値除去 ↔ elimination¶
h : P ↔ Q があるなら,h.mp : P → Q と h.mpr : Q → P を取り出せます.
名前の mp は modus ponens,mpr はその逆向きを表します.
証明図:
具体例: 「\(x = 0\) と \(x^2 = 0\) は同値である」が分かっているなら,\(x = 0\) から \(x^2 = 0\) を得られます. 逆向きに,\(x^2 = 0\) から \(x = 0\) を得ることもできます.
example (P Q : Prop) (h : P ↔ Q) (hP : P) : Q := by
exact h.mp hP
example (P Q : Prop) (h : P ↔ Q) (hQ : Q) : P := by
exact h.mpr hQ
古典論理: 排中律¶
Lean の基本的な推論規則は構成的に読めます.
構成的論理では,一般の命題 P について P ∨ ¬ P を無条件に証明することはできません.
そのため,排中律 P ∨ ¬ P や背理法を使うときは,古典論理を使っていることを意識します.
命題ごとの排中律は Classical.em P で得られます.
Mathlib では古典論理を使う定理や tactic がよく使われますが,ここでは構成的に証明できる規則と古典論理に依存する規則を分けて見ることが重要です.
証明図:
ここで LEM とは排中律(Law of Excluded Middle)のことです.
具体例: 古典論理では,任意の命題 \(P\) について「\(P\) が成り立つ,または \(P\) は成り立たない」と言えます. たとえば「ある方程式に解が存在する,または存在しない」という形の主張です.
古典論理: 背理法¶
古典論理では,¬ P を仮定すると矛盾が出ることから P を結論できます.
Core Lean では,この原理は Classical.byContradiction として使えます.
Classical.byContradiction は,¬ P → False から P を返します.
これは二重否定除去 ¬¬ P → P と同じ強さをもつため,一般には古典論理の原理です.
Mathlib を import している環境では,背理法用の便利な tactic として by_contra もよく使われますが,ここでは Core Lean の定理を直接使います.
証明図:
ここで RAA とは背理法(Reductio Ad Absurdum)のことです.
具体例: 「素数は無限に存在する」を背理法で示すとき,「素数は有限個しか存在しない」と仮定し,その仮定から矛盾を導いて,もとの命題を結論します.
述語論理¶
述語論理では,命題変数だけでなく,対象の型 α : Type と,その対象に依存する命題
P : α → Prop を扱います.
P a は「対象 a : α が性質 P を満たす」という命題です.
Lean の ∀ x : α, P x は,依存関数型,つまり「各 x : α に対して P x の証明を返す関数型」です.
そのため,全称命題の証明 h : ∀ x : α, P x は,関数のように具体的な項 a : α に適用して h a : P a を得ます.
一方,∃ x : α, P x は,証拠(witness) x : α とその証拠が性質を満たす証明 P x の組です.
全称導入 ∀ introduction¶
∀ x : α, Q x を証明するには,任意の x : α を 1 つ取って Q x を証明します.
Lean では intro x により,全称量化された変数を仮定として導入します.
ここで重要なのは,導入した x は特別な性質を仮定していない「任意の」対象だという点です.
自然演繹では,この条件を固有変数条件と呼びます.
証明図:
具体例: 「任意の実数 \(x\) について \(x^2 \ge 0\)」を示すには,特別な性質を仮定しない任意の実数 \(x\) を 1 つ取り,その \(x\) について \(x^2 \ge 0\) を示します.
example (α : Type) (P Q : α → Prop)
(hPQ : ∀ x : α, P x → Q x) (hP : ∀ x : α, P x) :
∀ x : α, Q x := by
intro x
exact hPQ x (hP x)
全称除去 ∀ elimination¶
h : ∀ x : α, P x があるなら,具体的な a : α に代入して h a : P a を得られます.
Lean では,全称命題の証明を関数のように適用します.
これは Curry--Howard 対応のもとで,全称量化が依存関数型として扱われていることの現れです.
証明図:
具体例: 「任意の実数 \(x\) について \(x^2 \ge 0\)」が分かっていれば,具体的な実数 \(a\) に代入して「\(a^2 \ge 0\)」を得られます.
存在導入 ∃ introduction¶
∃ x : α, P x を証明するには,具体的な証拠 a : α と,その証拠が性質を満たす証明 ha : P a を与えます.
Lean では Exists.intro a ha と書けます.
証明図:
具体例: 「\(2\) は偶数である」と分かっていれば,証拠として \(2\) を与えることで「偶数が存在する」と証明できます.
example (α : Type) (P : α → Prop) (a : α) (ha : P a) : ∃ x : α, P x := by
exact Exists.intro a ha
example (α : Type) (P : α → Prop) (a : α) (ha : P a) : ∃ x : α, P x := by
exists a
存在除去 ∃ elimination¶
hExists : ∃ x : α, P x から結論を得るには,証拠 x と証明 hx : P x を取り出し,そのもとで結論を示します.
Lean では cases hExists with で存在命題を分解できます.
取り出した証拠の名前はその枝の中だけで使える局所的な名前です.
したがって,結論そのものはその名前に依存してはいけません.
これも自然演繹では固有変数条件として表されます.
証明図:
具体例: 「正の実数が存在する」と分かっていて,さらに任意の正の実数 \(x\) から \(x^2 > 0\) が従うなら,存在する証拠を 1 つ取り出して「平方が正である実数が存在する」と結論できます.
example (α : Type) (P Q : α → Prop)
(hExists : ∃ x : α, P x) (hPQ : ∀ x : α, P x → Q x) :
∃ x : α, Q x := by
cases hExists with
| intro x hx =>
exact Exists.intro x (hPQ x hx)
全称と連言の組み合わせ¶
量化子と命題論理の規則は組み合わせて使います.
たとえば,すべての x について P x ∧ Q x が成り立つなら,すべての x について P x が成り立ち,すべての x について Q x が成り立ちます.
この例の証明図:
Lean のコードでは,constructor で連言の 2 つの成分を別々に証明します.
それぞれの枝で intro x により任意の x を導入し,h x : P x ∧ Q x から .left または .right で必要な成分を取り出します.
具体例: 集合 \(A\) のすべての元が「有理数であり,かつ正である」と分かっているなら,「すべての元が有理数である」と「すべての元が正である」を別々に取り出せます.
example (α : Type) (P Q : α → Prop) (h : ∀ x : α, P x ∧ Q x) :
(∀ x : α, P x) ∧ (∀ x : α, Q x) := by
constructor
· intro x
exact (h x).left
· intro x
exact (h x).right
存在と選言の組み合わせ¶
存在命題を使う証明では,まず証拠を取り出してから,命題論理の規則を適用することがよくあります.
次の例では,∃ x, P x と ∀ x, P x → Q x ∨ R x から,∃ x, Q x ∨ R x を示します.
この例の証明図:
存在除去で得た局所的な証拠 x は,そのまま結論の存在命題の証拠として再利用できます.
このとき外に出しているのは局所変数そのものではなく,Exists.intro x ... で包み直した存在命題の証明です.
具体例: ある整数 \(n\) が存在し,任意の整数は偶数または奇数であると分かっているなら,証拠 \(n\) を取り出して「偶数または奇数である整数が存在する」と結論できます.
example (α : Type) (P Q R : α → Prop)
(hExists : ∃ x : α, P x) (hPQR : ∀ x : α, P x → Q x ∨ R x) :
∃ x : α, Q x ∨ R x := by
cases hExists with
| intro x hx =>
exact Exists.intro x (hPQR x hx)
等号導入 = introduction¶
等号つきの述語論理では,任意の項は自分自身に等しいです.
Lean では反射律を rfl で証明します.
rfl は単に文字列として同じ式だけでなく,定義展開や計算によって同じ式になる等式も閉じられます.
このような同一性を定義的に等しい(definitionally equal)と呼びます.
証明図:
具体例: 任意の数 \(a\) について,\(a = a\) は反射律により成り立ちます.
等号除去: 置換¶
hEq : a = b があり,ha : P a があるなら,等しいものは同じ性質を満たすので P b が得られます.
Lean では hEq ▸ ha により,ha の型に現れる a を b に置き換えます.
反対向きに置換したいときは,等式を逆向きに使います.
たとえば ← hEq は b を a に戻す向きの等式として使えます.
証明図:
具体例: \(a = b\) と \(a > 0\) が分かっていれば,等しいものは同じ性質を持つので \(b > 0\) と結論できます.
等号除去: 書き換え¶
等式はゴールの書き換えにも使えます.
rw [hEq] は,ゴール中の左辺を右辺に書き換えます.
仮定を書き換える場合は rw [hEq] at h のように書きます.
反対向きに書き換える場合は rw [← hEq] を使います.
次の例では,ゴール f a = f b の左辺に現れる a を b に書き換えることで,f b = f b になり,反射律で閉じられます.
証明図:
具体例: \(a = b\) が分かっていれば,同じ関数 \(f\) を両辺に適用して \(f(a) = f(b)\) と書き換えられます. たとえば \(a = b\) から \(\sin a = \sin b\) が従います.
example (α β : Type) (f : α → β) (a b : α) (hEq : a = b) : f a = f b := by
rw [hEq]
example (α β : Type) (f : α → β) (a b : α) (hEq : a = b) : f b = f a := by
rw [← hEq]
example (α β : Type) (f : α → β) (a b : α) (c : β) (hEq : a = b) (h : f a = c) : f b = c := by
rw [hEq] at h
exact h
まとめ¶
命題論理の基本規則は,Lean では intro,exact,constructor,cases,False.elim などに対応します.
述語論理では,∀ を関数のように適用し,∃ を証拠とその証明の組として扱います.
以降の数学の形式化では,これらの規則を明示的に使うだけでなく,rw,simp,apply などのタクティックで同じ推論をより短く書くこともあります.
演習問題¶
この章の演習では,まず証明図や自然言語の証明を考えてから Lean の tactic に翻訳してください.
constructor,cases,intro,exact,False.elim,rw を意識して使います.
-
連言の可換性を証明してください.
-
選言の可換性を証明してください.
-
modus ponens を Lean で書いてください.
-
Falseから任意の命題が従うことを証明してください. -
全称命題を使って具体的な元に関する結論を得てください.
-
存在命題から証拠を取り出し,同じ証拠で別の存在命題を作ってください.
-
等式を使って命題を書き換えてください.
-
証明図を自分で書いてから,次の Lean 証明を完成させてください.
Chapter 02: Lean の基本構文・型・データ構造¶
この章では,Lean のファイルを読むために必要な基本語彙を整理します. 前章では命題論理・述語論理の証明規則を見ました. ここでは,それらの証明を書くための「言語としての Lean」を見ます.
扱う内容は次の通りです.
- コマンドと式
def,fun,example,theorem,lemmavariable,section,引数に現れる( ),{ },[ ]notationProp,Type,Sortと universe- 関数型・依存関数型
inductive型,structure,class- Chapter 01 で使った論理記号の実体
namespace,openabbrev- 積,直和型(非交和),
Option,Listなどのデータ構造 if,match,let,doなどの制御構文
Lean では「プログラム」と「証明」は同じ構文で書かれます. 自然数を返す関数も,命題の証明も,どちらも型をもつ項です. したがって,Lean のファイルを読むときは,まず「いま見ているものは環境に名前を追加するコマンドなのか,それとも型をもつ式なのか」を区別すると見通しがよくなります.
もう 1 つ重要なのは,「型」と呼ばれているものをいくつかの観点に分けて見ることです.
Prop や Type は型が住む階層です.
α → β や ∀ x : α, P x は関数型・依存関数型です.
Nat,List α,Option α,P ∨ Q,∃ x, P x は inductive 型です.
α × β,P ∧ Q,Subtype は structure として実装された積型です.
LE α や Add α は型クラスで,≤ や + の意味を型ごとに与えます.
この章では,Chapter 01 で使った And,Or,Eq,LE,LT,True,False,Not,Exists,∀ なども,
単なる論理記号ではなく,Lean の具体的な定義・構文・型クラスとして見直します.
コマンドと式¶
Lean ファイルはコマンドの列です.
import,def,theorem,inductive,structure,class,instance などはコマンドです.
一方,3,3 + 4,fun n => n + 1,Nat,P → Q などは式です.
式は Lean によって elaboration され,型が決まります. elaboration とは,ユーザが書いた構文や notation を,型情報を補いながら Lean の内部で扱う型付きの式へ変換する処理です. この処理を担当する部分を elaborator と呼びます. elaborator の詳しい仕組みは Chapter 06 で扱うので,ここでは「省略された引数や型クラス引数を補い,notation を実際の定義へ結びつける役割を持つ」と理解しておけば十分です. コマンドは式そのものではなく,名前つき定義を追加する,型を確認する,評価する,名前空間に入る,といった操作を環境に対して行います.
たとえば def コマンドは,名前つきの定義を環境に追加します.
#check は式の型を確認するためのコマンドで,#eval は計算できる式を評価するためのコマンドです.
example は,名前を残さずに小さな項や証明を型検査するための匿名の宣言です.
def typedNat : Nat := 3
def typedProposition : Prop := 2 + 2 = 4
#check typedNat
#check typedProposition
#check (fun n : Nat => n + 1)
#eval typedNat + 4
typedNat : Nat := 3 は,「typedNat という名前を定義し,その型は Nat,値は 3 である」と読みます.
同様に,typedProposition : Prop := 2 + 2 = 4 は,「typedProposition という名前をもつ命題を定義する」と読みます.
型注釈 : Nat は省略できることもありますが,講義資料では意図を明示するために書くことが多いです.
この講義資料の演習では,未完成の項や証明を表すために sorry を使うことがあります.
sorry はその場のゴールを仮に閉じるための穴で,Lean はその宣言に warning を出します.
したがって,演習中に「ここをあとで埋める」という印として使うことはできますが,完成した定義や定理には残さないものです.
sorry と tactic モードの関係は Chapter 03 で改めて扱います.
def¶
def は計算内容をもつ定義を作るコマンドです.
関数,値,命題の略記などを定義できます.
def name : T := t は,「name という名前に,型 T の項 t を結びつける」と読みます.
def addTwo (n : Nat) : Nat :=
n + 2
example : addTwo 3 = 5 := by
rfl
def IsPositiveNat (n : Nat) : Prop :=
0 < n
example : IsPositiveNat 3 := by
unfold IsPositiveNat
decide
def で定義した名前は,必要に応じて展開されます.
rfl で証明できる等式は,定義を展開して両辺が同じ形になる等式です.
この「定義を展開して同じになる」という同一性を,定義的な等しさと呼びます.
def で関数を定義すると,その計算規則を rfl や simp が利用できることがあります.
fun¶
fun x => t は無名関数を作る式です.
数学の notation では(関数名を明記しない) \(x \mapsto t\) に対応します.
def が名前つきの関数を環境に追加するコマンドであるのに対して,fun はその場で関数という項を作る式です.
Lean の多引数関数は基本的にカリー化されています.
たとえば Nat → Nat → Nat は,自然数を 1 つ受け取って,さらに Nat → Nat 型の関数を返す型として読めます.
fun x y => t は,おおよそ fun x => fun y => t の短い書き方です.
#check (fun n : Nat => n + 1)
#check (fun m n : Nat => m + n)
#check (fun (P Q : Prop) (hP : P) (_hQ : Q) => hP)
def addByFun : Nat → Nat → Nat :=
fun m n => m + n
example : addByFun 2 5 = 7 := by
rfl
def addConst (k : Nat) : Nat → Nat :=
fun n => n + k
example : addConst 4 3 = 7 := by
rfl
関数を引数に取る関数へ,その場で関数を渡すときにも fun がよく使われます.
これは後で tactic を読むときにも重要です.
たとえば含意 P → Q の証明は,P の証明を受け取って Q の証明を返す関数です.
def applyTwice (f : Nat → Nat) (n : Nat) : Nat :=
f (f n)
example : applyTwice (fun n => n + 1) 10 = 12 := by
rfl
def composeNat (f g : Nat → Nat) : Nat → Nat :=
fun n => f (g n)
example : composeNat (fun n => n + 1) (fun n => 2 * n) 5 = 11 := by
rfl
example (P Q : Prop) : P → Q → P :=
fun hP _hQ => hP
fun は tactic ではなく式です.
したがって,exact fun hP => ... のように書けば,「関数を証明項として直接与える」ことにもなります.
この見方は Chapter 03 で tactic と証明項の対応を見るときに使います.
演習¶
fun を使って,次の関数と証明項を書いてください.
def squareByFunExercise : Nat → Nat :=
-- `n ↦ n * n`
sorry
example : squareByFunExercise 5 = 25 := by
sorry
def applyToThreeExercise (f : Nat → Nat) : Nat :=
-- `f` を `3` に適用する.
sorry
example : applyToThreeExercise (fun n => n + 4) = 7 := by
sorry
example (P Q : Prop) : P → Q → Q :=
-- `fun` で証明項を書く.
sorry
example,theorem,lemma¶
example は名前を残さない匿名の宣言です.
構文やタクティックの小さな実験に向いています.
証明の文脈では example : P := ... は「命題 P の証明をその場で与える」という意味です.
ただし Lean のコマンドとしての example は,命題に限らず任意の型の項を匿名で確認する用途にも使えます.
命題そのものも式です.
次の最初の例では,2 + 2 = 4 という命題が Prop 型の式であることを示しています.
次の例では,その命題の証明を与えています.
theorem は名前つきの定理を宣言します.
後からその名前を使って参照できます.
theorem で定義される名前は,ある命題 P : Prop を型にもつ項,つまり P の証明項です.
theorem や lemma の型は命題でなければなりません.
データや計算内容を名前つきで定義したいときは def を使います.
theorem add_zero_named (n : Nat) : n + 0 = n := by
exact Nat.add_zero n
theorem two_plus_two_is_four : 2 + 2 = 4 := by
rfl
theorem positive_three_named : 0 < 3 := by
decide
example : 5 + 0 = 5 := by
exact add_zero_named 5
example : 2 + 2 = 4 := by
exact two_plus_two_is_four
example : 0 < 3 := by
exact positive_three_named
実用上は,補助的な定理を「lemma」と呼ぶことがよくあります.
Core Lean では,補助的な定理も theorem で宣言できます.
Mathlib を import している環境では lemma というコマンドもよく使われますが,ここでは Core Lean に合わせて theorem で書きます.
theorem zero_add_named (n : Nat) : 0 + n = n := by
exact Nat.zero_add n
example : 0 + 5 = 5 := by
exact zero_add_named 5
example,theorem,lemma はいずれも,命題を証明するためのコマンドです.
ただし,example は上で見たように,匿名の型検査例にも使えます.
命題証明として使う場合の違いは,主に「名前を残すか」「数学的にどのような位置づけか」です.
def との関係も重要です.
Lean の内部では,定義も定理も「名前に型つきの項を結びつける」という点では似ています.
たとえば def で Nat 型の値を定義することも,Prop 型の証明を定義することもできます.
ただし,数学的な命題の証明には,意図が明確になるように theorem や lemma を使うのが普通です.
variable と section¶
variable コマンドは,以降の定義や定理で使う変数をまとめて宣言します.
これは前章の証明図に出てきた Γ,つまり「現在使ってよい変数や仮定の集まり」に対応します.
section ... end で囲むと,その中だけで有効な変数を宣言できます.
宣言された変数は,後続の定義で実際に使われたとき,その定義の引数として自動的に追加されます.
たとえば次の pairFromVariables は,内部的には α,x,y を引数にもつ定義として扱われます.
section VariableExamples
variable (α : Type)
variable (x y : α)
def pairFromVariables : α × α :=
(x, y)
example : pairFromVariables Nat 2 5 = (2, 5) := by
rfl
end VariableExamples
波括弧 {α : Type} で宣言した引数は,Lean が推論できる場合には省略できる暗黙引数になります.
丸括弧 (α : Type) は通常の明示的な引数,波括弧 {α : Type} は暗黙引数です.
次の singletonList 3 では,要素 3 の型から α = Nat が推論されます.
section ImplicitVariableExamples
variable {α : Type}
def singletonList (x : α) : List α :=
[x]
example : singletonList 3 = [3] := by
rfl
example : singletonList (-2 : Int) = [(-2 : Int)] := by
rfl
end ImplicitVariableExamples
引数に現れる ( ),{ },⦃ ⦄,[ ]¶
Lean の定義や定理では,引数の括弧に意味があります. Mathlib の定理を読むときに重要なので,ここで整理しておきます.
(x : α): 明示的な引数です.通常,関数や定理を使う側が値を与えます.{α : Type}: 暗黙引数です.Lean が周囲の式から推論できるなら,通常は書きません.⦃x : α⦄: strict implicit 引数です.暗黙引数の一種で, Lean が後続の明示的な引数などから推論できるときに補います.[Add α]: 型クラス引数です.Lean が登録済みのinstanceから自動的に探します.
たとえば (h : P) は「命題 P の証明 h を明示的な引数として受け取る」という意味です.
一方,{α : Type} は命題の仮定ではなく,推論される型パラメータです.
⦃x : α⦄ も推論される引数ですが,通常の暗黙引数よりも,後ろに続く明示的な引数から値が決まる場面を意識した書き方です.
入力するときは {{x : α}} と書くこともできますが,pretty printer では ⦃x : α⦄ と表示されます.
[Add α] も通常の変数というより,「α には足し算がある」という構造を型クラス探索で要求している,と読みます.
型クラスについては,後の class と instance の節で改めて扱います.
def explicitId (α : Type) (x : α) : α :=
x
def implicitId {α : Type} (x : α) : α :=
x
def strictImplicitExample ⦃x : Nat⦄ (_h : x = x) : Nat :=
x
def addWithClass {α : Type} [Add α] (x y : α) : α :=
x + y
#check explicitId
#check implicitId
#check @implicitId
#check strictImplicitExample
#check @strictImplicitExample
#check addWithClass
example : explicitId Nat 3 = 3 := by
rfl
example : implicitId 3 = 3 := by
rfl
example : strictImplicitExample (show (3 : Nat) = 3 from rfl) = 3 := by
rfl
example : strictImplicitExample (x := 3) rfl = 3 := by
rfl
example : (∀ ⦃n : Nat⦄, n = n → n = n) := by
intro n h
exact h
example (h : ∀ ⦃n : Nat⦄, n = n → n = n) : (3 : Nat) = 3 := by
exact h rfl
example : addWithClass 2 5 = 7 := by
rfl
暗黙引数を明示したいときは,名前付き引数 (α := Nat) を使うことがあります.
また,@implicitId のように名前の前に @ を付けると,通常は隠れている暗黙引数も含めた型を確認できます.
strict implicit 引数も,必要なら (x := 3) のように名前付き引数で明示できます.
最初は「丸括弧は普通に渡す,引数」,「波括弧は Lean が推論する,引数」, 「二重波括弧は,後続の情報から推論される暗黙引数」, 「角括弧は型クラス探索で探す,構造」と覚えておけば十分です.
notation¶
Lean では,標準的な定義だけでなく,数学に近い記号や専用の構文を定義できます.
たとえば α → β は関数型の notation,P ∧ Q は And P Q の notation,x ∈ s は membership の notation です.
このような notation は,elaboration の過程で既存の定義や関数を使う式へ結びつけられます.
小さな notation なら,notation や infix で定義できます.
notation "twice(" n ")" => n + n
example : twice(3) = 6 := by
rfl
infixl:65 " +++ " => Nat.add
example : 2 +++ 3 = 5 := by
rfl
notation "twice(" n ")" => n + n は,twice(3) という notation を 3 + 3 と読むようにする指定です.
infixl:65 " +++ " => Nat.add は,中置 notation x +++ y を Nat.add x y と読むようにする指定です.
65 は優先順位,infixl の l は左結合を表します.
実際の Mathlib では,ℝ,∑,∈,⊔,→+* など,多くの notation が定義されています.
notation は読みやすさのための糖衣構文なので,分からない notation に出会ったら,まず #check で型を確認し,「これはどの定義の別表記か」を調べるのが有効です.
講義資料では notation を使いますが,必要に応じて元の定義名も併記します.
Type,Prop,Sort¶
まず,型そのものがどの階層に住んでいるかを整理します.
この節で扱う Type,Prop,Sort は「型の構成方法」ではなく,「型がどの階層にあるか」を表す語です.
Type は通常のデータ型が住む階層です.
たとえば Nat : Type,Int : Type,List Nat : Type です.
また Type 自身にも型があり,Type : Type 1 です.
Type : Type としてしまうと自己言及的な循環が起きるため,Lean では Type 0 : Type 1,Type 1 : Type 2,... という階層を使います.
Prop は命題が住む階層です.
P : Prop の項は,命題 P の証明です.
Sort は Prop と Type u をまとめて扱うためのより一般的な階層で,おおまかに Sort 0 が Prop,Sort (u + 1) が Type u に対応します.
#check Prop
#check Type
#check Type 1
#check Sort 0
#check Sort 1
universe u v
def sampleNumber : Nat := 42
def sampleList : List Nat := [1, 2, 3]
example : sampleList.length = 3 := by
rfl
def idSort (α : Sort u) (x : α) : α :=
x
example : idSort Nat 7 = 7 := by
rfl
example (P : Prop) (h : P) : idSort P h = h := by
rfl
def idType (α : Type u) (x : α) : α :=
x
example : idType Nat 5 = 5 := by
rfl
example : idType (List Nat) [1, 2] = [1, 2] := by
rfl
idSort は Prop と Type の両方に使えます.
一方,通常の数学的対象を引数に取る関数では α : Type u と書くことが多いです.
この章では universe 変数を明示していますが,Lean が自動的に universe を推論してくれる場面も多くあります.
演習¶
#check で次の式の型を確認してください.
関数型と依存関数型¶
型 α β : Type から構成される型 α → β は関数型です.
たとえば Nat → Nat は,自然数を受け取って自然数を返す型です.
関数型の項は式 fun x => t のように書けます.これは無名関数を作る式で,数学の \(x \mapsto t\) に対応します.
def simpleTypedFunction : Nat → Nat :=
fun n => n + 1
example : simpleTypedFunction 4 = 5 := by
rfl
example (P Q : Prop) (hPQ : P → Q) (hP : P) : Q :=
hPQ hP
より一般には,返り値の型が入力の値に依存することがあります.
これを依存関数型と呼びます.
Lean では (x : α) → β x と書けます.
特に全称命題は,結論が命題であるような依存関数型 (x : α) → P x です.
例として,入力 n に応じて長さ n のベクトルを返す関数を考えます.
Lean では,長さ n の Nat ベクトルを Vector Nat n と書けます.
したがって (n : Nat) → Vector Nat n は,n を受け取ると「長さ n の自然数ベクトル」を返す型です.
これは返り値の型そのものが入力 n に依存しているので,依存関数型の例になっています.
次の zeroVec n は,0 を n 回並べたベクトルです.
表示するときは toList で通常のリストに戻して確認します.
def zeroVec (n : Nat) : Vector Nat n :=
Vector.replicate n 0
#check Vector
#check Vector.replicate
#check zeroVec
#check (zeroVec 4)
#eval (zeroVec 4).toList
example : (zeroVec 4).toList = [0, 0, 0, 0] := by
rfl
#check ((n : Nat) → Vector Nat n)
なお,通常の List Nat でも「0 が並んだリスト」を作れますが,型は常に List Nat なので,長さ n は型には現れません.
Vector Nat n を使うと,長さ n であることが返り値の型に入ります.
次の IsEvenNat n は,「自然数 n が偶数である」という命題です.
これは真偽値 Bool を返す判定関数ではなく,証明すべき命題を返す述語です.
def IsEvenNat (n : Nat) : Prop :=
∃ k : Nat, n = 2 * k
example : IsEvenNat 4 := by
unfold IsEvenNat
exact ⟨2, rfl⟩
ここで ⟨2, rfl⟩ は,「具体的な値」と「その値が条件を満たす証明」をまとめたデータです.
「証拠として k = 2 を選び,4 = 2 * 2 は rfl で示せる」という意味です.
山括弧 ⟨...⟩ は,型が要求している部品を順に与えて値や証明を作るための notation です.
より正確には constructor を使う省略記法ですが,constructor 一般の説明は structure と inductive 型の節で扱います.
次の全称命題
∀ n : Nat, IsEvenNat (2 * n) は,各自然数 n に対して命題 IsEvenNat (2 * n) の証明を返す依存関数型です.
この見方が,Lean で「全称命題の証明を関数のように適用する」理由です.
#check (∀ n : Nat, IsEvenNat (2 * n))
theorem twice_is_even (n : Nat) : IsEvenNat (2 * n) := by
unfold IsEvenNat
exact ⟨n, rfl⟩
example : IsEvenNat (2 * 7) := by
exact twice_is_even 7
演習¶
自然数を 2 倍する関数を def で定義し,簡単な計算例を証明してください.
def twiceExercise (n : Nat) : Nat :=
-- ここを埋める.
sorry
example : twiceExercise 4 = 8 := by
-- 定義通りなら `rfl` で閉じる.
sorry
structure¶
structure は,複数の field をもつデータ型を定義するコマンドです.
数学では,点,群,位相空間,線形写像などを structure として表すことが多くあります.
structure は,名前つき field をもつ積型と考えると分かりやすいです.
実際,Lean の内部では structure は単一の constructor をもつ inductive 型として扱われ,そこに field や射影関数の情報が登録されています.
そのため inductive 型と同様に再帰的な定義も受け付けますが,再帰的な出現は strictly positive でなければなりません.
field 名に対して射影関数が自動で作られ,p.x のようなドット notation でアクセスできます.
deriving Repr は,PointStruct の値を Lean が表示できるようにする指定です.
#eval で値を確認したいときなどに使われます.
deriving DecidableEq は,2 つの PointStruct が等しいかどうかを判定する手続きを自動生成する指定です.
たとえば,2 つの点の座標がどちらも等しいかを計算で判定できるようになります.
この定義では,x と y が field です.
field は,その型の値が持つ名前つきの成分です.
一方,constructor はその型の値を作るものです.
structure では通常 PointStruct.mk という constructor が作られ,{ x := 1, y := 2 } はそれを使って点を作る notation です.
#check PointStruct.mk
example : PointStruct.mk 1 2 = { x := 1, y := 2 } := by
rfl
def originStruct : PointStruct :=
{ x := 0, y := 0 }
def PointStruct.swap (p : PointStruct) : PointStruct :=
{ x := p.y, y := p.x }
example : originStruct.x = 0 := by
rfl
example : PointStruct.swap { x := 1, y := 2 } = { x := 2, y := 1 } := by
rfl
PointStruct.x や PointStruct.y は,structure の中で宣言された field です.
一方,PointStruct.swap は field ではなく,あとから PointStruct という名前空間に置いた普通の関数です.
Lean では,名前空間にある関数や定理も,最初の明示的な引数の型からドット notation で呼べることがあります.
そのため,PointStruct.swap p は p.swap とも書けます.
このような関数は,プログラミング言語のメソッドのように見えますが,
Lean の用語としては「名前空間にある関数」または「定理」をドット notation で適用している,と考えるのが安全です.
field かどうかは,それが structure や class の中で宣言された成分かどうかで判断します.
#check PointStruct.x
#check PointStruct.swap
example (p : PointStruct) : PointStruct.swap p = p.swap := by
rfl
def addPointStruct (p q : PointStruct) : PointStruct :=
{ x := p.x + q.x, y := p.y + q.y }
example : addPointStruct { x := 1, y := 2 } { x := 3, y := 4 } = { x := 4, y := 6 } := by
rfl
標準ライブラリでは,データの積 Prod も structure です.
α × β は Prod α β の notation で,p.1 と p.2 は field を取り出す射影です.
#check Prod
#check Prod.mk
#check Prod.fst
def sampleProduct : Nat × Nat :=
(3, 5)
example : sampleProduct.1 = 3 := by
rfl
example : sampleProduct.2 = 5 := by
rfl
def sampleTriple : Nat × Nat × Nat :=
(3, 5, 8)
example : sampleTriple.1 = 3 := by
rfl
example : sampleTriple.2.1 = 5 := by
rfl
example : sampleTriple.2.2 = 8 := by
rfl
命題の連言 P ∧ Q も And P Q という structure です.
h : P ∧ Q から h.left と h.right を取り出すことは,structure の field を取り出すことと同じ形です.
ここで intro :: は,この structure の constructor 名を intro にする指定です.
つまり,hP : P と hQ : Q から And.intro hP hQ : P ∧ Q を作れます.
一方,left と right は field であり,作られた証明から左右の証明を取り出すために使われます.
#check And
#check And.intro
#check And.left
#check Subtype
example (P Q : Prop) (hP : P) (hQ : Q) : P ∧ Q :=
And.intro hP hQ
example (P Q : Prop) (h : P ∧ Q) : P :=
h.left
命題の同値 P ↔ Q も Iff P Q という structure です.
h : P ↔ Q からは,h.mp : P → Q と h.mpr : Q → P を取り出せます.
これは同値命題を,両方向の含意を field としてもつ structure として表しているということです.
structure Iff (a b : Prop) : Prop where
intro ::
mp : a → b
mpr : b → a
#check Iff
#check Iff.intro
#check Iff.mp
#check Iff.mpr
example (P Q : Prop) (hPQ : P → Q) (hQP : Q → P) : P ↔ Q :=
Iff.intro hPQ hQP
example (P Q : Prop) (h : P ↔ Q) (hP : P) : Q :=
h.mp hP
演習¶
2 つの成分を持つ structure を定義し,field を取り出してください.
また,3 つの積 α × β × γ から各成分を取り出す式を書いてください.
structure PointExercise where
x : Nat
y : Nat
def pExercise : PointExercise :=
{ x := 2, y := 5 }
example : pExercise.x = 2 := by
sorry
example (α β γ : Type) (x : α × β × γ) : α :=
sorry
inductive 型¶
inductive は,constructor によって項を作る型を定義するコマンドです.
Sum,Option,List,Or,Exists,Eq は代表的な inductive 型です.
Sum α β は左の型 α の値または右の型 β の値を持つ型で,α ⊕ β と書けます.数学の非交和 \(\alpha \sqcup \beta\) に対応します.
Option α は値がある場合とない場合を表します.
List α は有限列です.
Or は命題の選言を表し,P ∨ Q と書けます.
Exists は存在命題を表し,∃ x, P x と書けます.
Eq は等式命題を表し,a = b と書けます.
実際の定義は,おおよそ次のようになっています.
Sum と Exists は Init/Core.lean にあり,α ⊕ β は Sum α β の notation です.
inductive Sum (α : Type u) (β : Type v) where
| inl (val : α) : Sum α β
| inr (val : β) : Sum α β
@[inherit_doc] infixr:30 " ⊕ " => Sum
inductive Exists {α : Sort u} (p : α → Prop) : Prop where
| intro (w : α) (h : p w) : Exists p
Eq,Option,List,Or は Init/Prelude.lean にあります.
inductive Eq : α → α → Prop where
| refl (a : α) : Eq a a
inductive Option (α : Type u) where
| none : Option α
| some (val : α) : Option α
export Option (none some)
inductive List (α : Type u) where
| nil : List α
| cons (head : α) (tail : List α) : List α
inductive Or (a b : Prop) : Prop where
| inl (h : a) : Or a b
| inr (h : b) : Or a b
inductive 型を見るときは,次の 3 つを確認すると読みやすくなります.
- 型を作る部分: 何を入れると型ができるか.例:
List : Type u → Type u - constructor: その型の項をどう作るか.例:
List.nil,List.cons - 消去・場合分け:
match,cases,inductionでどう使えるか.
inductive は,型そのものが引数を取る形でも定義できます.
たとえば Option α や List α は,型 α を 1 つ受け取って新しい型を作ります.
この場合,#check で型名だけを見ると,値の型ではなく「型から型を作る関数」のように見えます.
inductive MyOption (α : Type) where
| none : MyOption α
| some (x : α) : MyOption α
#check MyOption
#check (MyOption : Type → Type)
#check MyOption Nat
#check MyOption.none
#check MyOption.some
上の #check MyOption は,おおよそ
という意味です.
Lean の表示では MyOption (α : Type) : Type のように,引数つきの形で表示されることもあります.
#check (MyOption : Type → Type) と型注釈を付けると,Type → Type と見えていることを明示できます.
これは MyOption だけではまだ具体的な値の型ではなく,
Nat や String などの型を受け取って MyOption Nat,MyOption String という型を作る,という意味です.
一方,#check MyOption Nat は
となります.
つまり MyOption Nat は,「自然数が入っているかもしれない値」の型です.
def myOptionNat : MyOption Nat :=
MyOption.some 3
def myOptionString : MyOption String :=
MyOption.none
def myOptionGetD {α : Type} (default : α) : MyOption α → α
| MyOption.none => default
| MyOption.some x => x
example : myOptionGetD 0 myOptionNat = 3 := by
rfl
example : myOptionGetD "empty" myOptionString = "empty" := by
rfl
このように,引数を取る inductive 型では,
まず MyOption Nat のように型引数を与えて具体的な型を作り,
その型の項を constructor で作ります.
List α や Option α も同じパターンです.
inductive Sign where
| negative
| zero
| positive
deriving Repr, DecidableEq
def signNeg : Sign → Sign
| Sign.negative => Sign.positive
| Sign.zero => Sign.zero
| Sign.positive => Sign.negative
example : signNeg Sign.positive = Sign.negative := by
rfl
example : signNeg Sign.zero = Sign.zero := by
rfl
def sumLeft : Nat ⊕ Int :=
Sum.inl 10
def sumRight : Nat ⊕ Int :=
Sum.inr (-10)
#check (Sum.inl 10)
#check (Sum.inr (-10))
def sumToInt (x : Nat ⊕ Int) : Int :=
match x with
| Sum.inl n => Int.ofNat n
| Sum.inr z => z
example : sumToInt (Sum.inl 7) = (7 : Int) := by
rfl
example : sumToInt (Sum.inr (-3)) = (-3 : Int) := by
rfl
def safePred (n : Nat) : Option Nat :=
match n with
| 0 => none
| m + 1 => some m
example : safePred 0 = none := by
rfl
example : safePred 4 = some 3 := by
rfl
def listExample : List Nat :=
[1, 2, 3].map (fun n => n + 1)
example : listExample = [2, 3, 4] := by
rfl
def headD {α : Type u} (default : α) : List α → α
| [] => default
| x :: _ => x
example : headD 0 [5, 6, 7] = 5 := by
rfl
example : headD 0 ([] : List Nat) = 0 := by
rfl
inductive 型は再帰的にも定義できます.
次の Tree α は,値をもつ葉と,左右の部分木をもつ節点からなる二分木です.
inductive Tree (α : Type u) where
| leaf (value : α)
| node (left right : Tree α)
def Tree.size {α : Type u} : Tree α → Nat
| Tree.leaf _ => 1
| Tree.node left right => Tree.size left + Tree.size right
example : Tree.size (Tree.node (Tree.leaf 1) (Tree.leaf 2)) = 2 := by
rfl
命題の選言 P ∨ Q は Or P Q,存在命題 ∃ x, P x は Exists (fun x => P x) という inductive 型です.
Chapter 01 で P ∨ Q の証明に left や right を使ったのは,内部的には Or.inl や Or.inr で項を作っているからです.
仮定 h : P ∨ Q に cases h を使うと 2 つのゴールに分かれるのは,Or の constructor が 2 つあるからです.
#check Or
#check Exists
#check Eq
example (P Q : Prop) (hP : P) : P ∨ Q :=
Or.inl hP
example (P Q R : Prop) (h : P ∨ Q) (hPR : P → R) (hQR : Q → R) : R :=
Or.elim h hPR hQR
example (n : Nat) : n = n :=
Eq.refl n
演習¶
また,Or と Exists を notation を使わずに作ってみてください.
example (P Q : Prop) (hP : P) : Or P Q := by
sorry
example : Exists (fun n : Nat => n = 2) := by
sorry
型クラス class と instance¶
class は,型クラスを定義するコマンドです.
型クラスは,「この型にはこの構造や操作がある」という情報を Lean に登録し,必要な場所で自動的に探すための仕組みです.
class は構文上は structure に近く,field をもつデータとして定義されます.
内部的には,通常の class ... where は structure と同様に inductive 型として扱われます.
ただし inductive が「constructor で項を作る型」を定義するコマンドであるのに対し,class はその型を型クラス探索の対象として登録し,[Add α] のような引数を Lean が自動で探せるようにする点が本質的に異なります.
instance は,特定の型に対してその field を埋めた値を登録するコマンドです.
標準ライブラリの LE,LT,Add も型クラスです.
class LE (α : Type u) where
le : α → α → Prop
class LT (α : Type u) where
lt : α → α → Prop
class Add (α : Type u) where
add : α → α → α
x ≤ y は LE.le x y の notation ですが,どの LE.le を使うかは x と y の型から決まります.
+ も同様に [Add α] から Add.add を取り出して使います.
たとえば LE.le (2 : Nat) 3 が書けるのは,Nat に対する LE Nat の instance があらかじめ登録されており,Lean がそれを型クラス探索で見つけるためです.
#check LE
#check LE.le
#check LT
#check LT.lt
#check Add
#check Add.add
example : LE.le (2 : Nat) 3 := by
decide
example : (2 : Nat) ≤ 3 := by
decide
example : Add.add (2 : Nat) 5 = 7 := by
rfl
example : (2 : Nat) + 5 = 7 := by
rfl
自分で定義した型にも,既存の数学的な型クラスの instance を登録できます.
次の Vec2 は整数成分の平面ベクトルです.
Vec2 自体は具体的なデータ型なので structure として定義し,class にする必要はありません.
class にするのは,Add のように「ある型に備わる操作や構造」を型クラス探索で扱いたい場合です.
instance : Add Vec2 を登録すると,u + v という notation が Vec2 に対して使えるようになります.
structure Vec2 where
x : Int
y : Int
deriving Repr, DecidableEq
instance : Add Vec2 where
add u v := { x := u.x + v.x, y := u.y + v.y }
def vecA : Vec2 :=
{ x := 1, y := 2 }
def vecB : Vec2 :=
{ x := 3, y := 4 }
example : vecA + vecB = { x := 4, y := 6 } := by
rfl
example : Add.add vecA vecB = { x := 4, y := 6 } := by
rfl
演習¶
型クラスを要求する関数の例として,加法を使う関数を定義してください.
また,≤ と < が型クラスの field であることを #check で確認してください.
def addThreeExercise {α : Type} [Add α] (a b c : α) : α :=
-- `a + b + c`
sorry
#check LE.le
#check LT.lt
論理記号の対応まとめ¶
Chapter 01 で使った論理記号は,ここまで見た依存関数型,structure,inductive 型,class の具体例として理解できます.
一覧にすると次のようになります.
| notation | 展開後の形 | 実装の種類 | 項を作る典型 | 情報を使う典型 |
|---|---|---|---|---|
P → Q |
非依存関数型 | 関数型 | fun hP => ... |
hPQ hP |
∀ x : α, P x |
依存関数型 | 依存関数型 | fun x => ... |
h x |
P ∧ Q |
And P Q |
structure |
And.intro hP hQ,⟨hP, hQ⟩ |
h.left,h.right |
P ∨ Q |
Or P Q |
inductive 型 |
Or.inl hP,Or.inr hQ |
cases h,Or.elim h ... ... |
∃ x : α, P x |
Exists (fun x => P x) |
inductive 型 |
Exists.intro w hw,⟨w, hw⟩ |
witness と証明に分解 |
a = b |
Eq a b |
inductive 型 |
Eq.refl a,rfl |
rw,subst |
P ↔ Q |
Iff P Q |
structure |
Iff.intro hPQ hQP,⟨hPQ, hQP⟩ |
h.mp,h.mpr |
True |
True |
inductive 型 |
True.intro |
ほぼ情報なし |
False |
False |
constructor なしの inductive 型 |
通常は作れない | False.elim h |
¬ P |
Not P,すなわち P → False |
def |
fun hP => ... |
hNot hP : False |
x ≤ y |
LE.le x y |
class LE の field |
型ごとの定理や計算 | 型ごとの定理や計算 |
x < y |
LT.lt x y |
class LT の field |
型ごとの定理や計算 | 型ごとの定理や計算 |
∀ だけは,And や Or のような通常の名前つき inductive 型ではありません.
Lean の構文として依存関数型へ elaboration されます.
section LogicConnectivesAsTypes
variable (P Q R : Prop)
#check (P → Q)
#check (∀ n : Nat, IsEvenNat n)
#check (P ∧ Q)
#check (And P Q)
#check (P ∨ Q)
#check (Or P Q)
#check (∃ n : Nat, IsEvenNat n)
#check (P ↔ Q)
#check (Iff P Q)
#check ((2 : Nat) = 2)
#check ((2 : Nat) ≤ 3)
#check ((2 : Nat) < 3)
example (hP : P) (hQ : Q) : P ∧ Q :=
And.intro hP hQ
example (h : P ∧ Q) : P :=
h.left
example (hPQ : P → Q) (hQP : Q → P) : P ↔ Q :=
Iff.intro hPQ hQP
example (h : P ↔ Q) (hP : P) : Q :=
h.mp hP
example : True :=
True.intro
example (h : False) : P :=
False.elim h
example (hP : P) : ¬ ¬ P :=
fun hNotP => hNotP hP
end LogicConnectivesAsTypes
namespace と open¶
namespace は,名前を整理するためのコマンドです.
大きなプロジェクトでは,同じような名前の定義がたくさん出てきます.
名前空間を使うと,Geometry.Point のように,どの分野・モジュールの名前かを明示できます.
namespace Geometry
structure Point where
x : Int
y : Int
deriving Repr, DecidableEq
def origin : Point :=
{ x := 0, y := 0 }
def reflectX (p : Point) : Point :=
{ x := p.x, y := -p.y }
example : reflectX origin = origin := by
rfl
end Geometry
名前空間の外から参照するときは,完全な名前 Geometry.Point や Geometry.origin を使います.
このファイル全体は namespace Chapter02 の中にあるので,厳密な完全名は Chapter02.Geometry.Point です.
ただし,同じ Chapter02 名前空間の中では Geometry.Point と書けます.
example : Geometry.Point :=
Geometry.origin
example : Geometry.reflectX Geometry.origin = Geometry.origin := by
rfl
open は,名前空間の中の名前を短く使えるようにするコマンドです.
open Geometry と書くと,そのスコープ内では Geometry.Point を Point,Geometry.origin を origin と書けます.
ただし,読み手にとって由来が分かりにくくなることもあるので,必要な範囲に限定して使うのがよいです.
section OpenExamples
open Geometry
def reflectedOrigin : Point :=
reflectX origin
example : reflectedOrigin = origin := by
rfl
end OpenExamples
namespace ... end は新しい名前空間に入る構文で,そこで定義した名前はその名前空間に属します.
一方,open は既存の名前空間を開いて,名前を短く参照するための構文です.
つまり,namespace は名前を作る場所を決め,open は名前を読むときの省略を許す,と考えるとよいです.
演習¶
namespace を使って,同じ名前の定義が衝突しないことを確認してください.
namespace AExercise
def value : Nat := 10
end AExercise
namespace BExercise
def value : Nat := 20
end BExercise
#check AExercise.value
#check BExercise.value
abbrev¶
abbrev は略記を作るコマンドです.
def と似ていますが,「新しい概念を作る」というより「長い型や式に短い名前をつける」という意図を表します.
Lean は abbrev を展開しやすい略記として扱うので,型の同一視や型推論で邪魔になりにくいです.
一方で,数学的に意味のある新しい概念や,後で定理を付けたい対象には def を使うことが多いです.
abbrev NatPair := Nat × Nat
def sumNatPair (p : NatPair) : Nat :=
p.1 + p.2
example : sumNatPair (2, 3) = 5 := by
rfl
NatPair は Nat × Nat の略記なので,Lean は両者をほとんど同じものとして扱います.
このため,Nat × Nat 型の値をそのまま NatPair として使えます.
制御構文¶
Lean の制御構文は,通常のプログラミング言語の構文に似ています. ただし Lean では,これらもすべて型をもつ式です.
if ... then ... else ...¶
if は条件分岐です.
条件には,Bool または Lean が真偽を判定できる命題が入ります.
n < 10 は命題ですが,自然数の大小比較には判定手続きがあるため if の条件として使えます.
一般の命題 P : Prop を条件にするには,Lean が [Decidable P] を見つけられる必要があります.
def minWithTen (n : Nat) : Nat :=
if n < 10 then n else 10
example : minWithTen 3 = 3 := by
rfl
example : minWithTen 20 = 10 := by
rfl
match¶
match は inductive 型やデータ構造を場合分けする式です.
Option,List,自分で定義した inductive などを分解できます.
各分岐は同じ型の式を返す必要があります.
証明で使う cases はゴールを場合分けする tactic ですが,match は式の中で場合分けして値を作る構文です.
def optionToNat : Option Nat → Nat
| none => 0
| some n => n
example : optionToNat none = 0 := by
rfl
example : optionToNat (some 8) = 8 := by
rfl
def listLength : List Nat → Nat
| [] => 0
| _ :: xs => 1 + listLength xs
example : listLength [10, 20, 30] = 3 := by
rfl
let¶
let は局所的な名前をつける構文です.
長い式を読みやすくするために使います.
let で導入した名前は,その式の残りの部分だけで使えます.
定義を環境に追加する def とは異なり,let は局所的な束縛です.
def squareSum (x y : Nat) : Nat :=
let x2 := x * x
let y2 := y * y
x2 + y2
example : squareSum 3 4 = 25 := by
rfl
do¶
do notation は,モナド的な計算を順番に書くための構文です.
最初は,失敗するかもしれない計算を Option でつなぐ例として見るとよいです.
do ブロックの各行も式を組み立てるための notation であり,最終的には bind や pure を使う式に展開されます.
def addOptions (x y : Option Nat) : Option Nat := do
let a ← x
let b ← y
pure (a + b)
example : addOptions (some 2) (some 5) = some 7 := by
rfl
example : addOptions none (some 5) = none := by
rfl
let a ← x は,x が some a なら中身を取り出して続行し,none なら全体を none にする,という動きをします.
同様に let b ← y で y が none なら,以降の pure (a + b) は実行されず,結果は none になります.
証明では do notation を頻繁に使うわけではありませんが,Lean でプログラムを書くときには重要です.
まとめ¶
Lean の基本は「すべての式には型がある」という考え方です.
Prop は命題,Type はデータ型,Sort はその両方を含む一般化です.
α → β と ∀ x : α, P x はどちらも関数型・依存関数型であり,命題として読むと含意や全称量化になります.
inductive は constructor で項を作る型を定義し,structure は名前つき field をもつ積型を定義し,class は structure を型クラス探索に登録できる形で定義します.
Chapter 01 で見た P ∧ Q,P ∨ Q,∃ x, P x,a = b,¬ P,x ≤ y,x < y は,それぞれ And,Or,Exists,Eq,Not,LE.le,LT.lt の notation として理解できます.
それぞれが structure,inductive,def,class のどれで実装されているかを見ると,constructor,cases,intro,rfl,rw などの tactic がどのような証明項を作っているかも見えやすくなります.
今後 Mathlib を読むときには,まず宣言が def なのか,theorem/lemma なのか,inductive なのか,structure なのか,class/instance なのかを見ると,対象の役割を把握しやすくなります.
Chapter 03: tactic を用いた証明¶
この章では,Lean で証明を書くための基本的な道具を整理します.
前章までに,命題,型,定義,関数型・依存関数型,structure,inductive 型,class などを見ました.
ここでは,それらの型の項として証明を構成する方法に焦点を当てます.
Lean では命題は型であり,証明はその型の項です.
したがって,「証明する」とは,まずは fun,constructor,関数適用,Eq.refl などを使って証明項を構成することです.
by ... による tactic モードは,その証明項をゴールの変化を見ながら組み立てるための書き方です.
特に次の内容を扱います.
- 証明項としての証明
fun,constructor,関数適用,Eq.reflsorryと未完成の証明項byと tactic モードの読み方- tactic と証明項の対応
exact,assumption,rfl,show,change,dsimpapply,intro,specializehave,sufficesrw,simp,simpacalcモードconstructor,obtain,rintro,left,right,match,cases,rcasesinductioncongr,ext,funextby_cases,Classical.byContradiction,exfalso,contradictiondecide,grind#check,exact?,rw?などの検索支援convモード- Lean における等号の証明パターン
- 命題外延性,選択,商,Lean の追加原理
ring,nlinarith,linarith などの代数・不等式向け tactic は,次章の Mathlib を使う証明で扱います.
証明項としての証明¶
命題 P : Prop は型であり,その証明は型 P の項です.
言い換えると,P : Prop は命題そのもの,hP : P は命題 P の証明,あるいは証明項です.
そのため,すでに hP : P を持っていれば,hP そのものが P の証明です.
含意 P → Q の証明は,P の証明を受け取って Q の証明を返す関数です.
したがって,fun hP => ... という関数として書けます.
全称命題 ∀ x : α, P x も,同じく入力 x に応じて証明 P x を返す依存関数です.
example (P Q : Prop) (hQ : Q) : P → Q :=
fun _hP => hQ
example (P : Nat → Prop) (h : ∀ n : Nat, P n) : P 0 :=
h 0
structure や inductive 型の命題は,constructor を使って証明項を作ります.
連言 P ∧ Q は And P Q という structure なので,And.intro hP hQ で証明できます.
山括弧 ⟨...⟩ は,constructor 名を省略して値や証明を作る notation です.
example (P Q : Prop) (hP : P) (hQ : Q) : P ∧ Q :=
And.intro hP hQ
example (P Q : Prop) (hP : P) (hQ : Q) : P ∧ Q :=
⟨hP, hQ⟩
example (P Q : Prop) (hP : P) : P ∨ Q :=
Or.inl hP
example : ∃ n : Nat, n + 2 = 5 :=
Exists.intro 3 rfl
example : ∃ n : Nat, n + 2 = 5 :=
⟨3, rfl⟩
等式 a = b は Eq a b という inductive 型です.
反射律 Eq.refl a は a = a の証明項です.
Lean は両辺を計算・定義展開して同じ式になる場合にも,Eq.refl や rfl を使えます.
#check Eq
#check Eq.refl
example : Eq 3 3 :=
Eq.refl 3
example : (fun n : Nat => n + 1) 2 = 3 :=
Eq.refl 3
証明を書いている途中では,未完成の証明項として sorry を一時的に置けます.
sorry はどんな型の項も仮に作ったことにしますが,Lean はその宣言に warning を出します.
したがって,演習問題や作業中の証明では便利ですが,完成した定義や定理には残さないものです.
このコードは型検査自体は通りますが,「この宣言は sorry を使っている」という警告が残ります.
sorry は証明探索の道具ではなく,あとで証明項に置き換えるべき穴です.
演習¶
fun や constructor を使って,次の証明項を tactic モードなしで書いてください.
example (P Q : Prop) (hP : P) : Q → P :=
sorry
example (P Q : Prop) (hP : P) (hQ : Q) : Q ∧ P :=
sorry
by と tactic モードの基本¶
by 以降に tactic を並べる書き方を tactic モードと呼びます.
Lean は現在のゴールを持っていて,各 tactic はそのゴールを変形したり閉じたりします.
VS Code の Infoview では,上側にローカルコンテキスト,下側に現在のゴールが表示されます.
tactic を 1 行実行するたびに,Lean は未解決のゴールを更新します.
すべてのゴールが閉じると,by ... 全体が証明項になります.
ここでいう tactic は,Lean のカーネルが直接持っている推論規則そのものではありません. tactic は,elaboration の途中で実行され,ゴールを操作しながら証明項を構成するためのプログラムです. 完成した証明項は,最後に Lean のカーネルによって型検査されます. この章では tactic の使い方に集中し,elaboration やカーネルの詳しい仕組みは Chapter 06 で扱います.
by は,example ... := by や theorem ... := by の直後だけに現れる特別な記号ではありません.
Lean がある場所で型 T の項を期待しているとき,そこに
と書くと,Lean は「型 T の項を tactic で作る」モードに入ります.
特に T : Prop のとき,これは「その場所で必要な証明を tactic で作る」という意味です.
一番直接的な tactic は exact です.
ゴールと同じ型をもつ項や証明をすでに持っているとき,それを exact に渡します.
厳密には,ゴールの型と exact に渡す項の型が定義的に等しいときに使えます.
assumption は,ローカルコンテキストの中からゴールと一致する仮定を探して使います.
rfl は,両辺が定義を展開すれば同じになる等式を閉じます.
「計算すれば同じ」という等式によく使います.
Lean の等式 a = b は Eq a b という型です.
rfl は Eq.refl,つまり反射律を使う tactic です.
反射律は本来 a = a を証明するものですが,Lean は両辺を計算・定義展開して同じ式になる場合にも rfl を受け入れます.
このように,計算や定義展開だけで同じと判定されることを「定義的に等しい(definitionally equal)」と言います.
#check Eq
#check Eq.refl
example : Eq 3 3 := by
rfl
example : (fun n : Nat => n + 1) 2 = 3 := by
exact Eq.refl 3
一方,数学的には正しい等式でも,定義的に等しくない場合は rfl では閉じません.
たとえば a + b = b + a は可換性の定理 Nat.add_comm を使って証明します.
このような「命題として証明された等式」は,rw などで書き換えに使います.
example (a b : Nat) : a + b = b + a := by
exact Nat.add_comm a b
def Positive (n : Nat) : Prop :=
0 < n
show は,現在のゴールを明示的に書く tactic です.
証明を読む人に「いま何を示しているか」を示すのに使えます.
show で書いた命題は,現在のゴールと定義的に同じでなければなりません.
change は,現在のゴールを定義的に等しい別の表示へ置き換える tactic です.
次の例では,Positive 3 を定義通り 0 < 3 に変えています.
論理的に同値な任意の命題へ変えられるわけではありません.
unfold も定義を展開します.
特定の名前を明示的に展開したいときに使います.
dsimp は definitional simplification の略で,定義を展開して計算で簡約できる部分を整理します.
MiL では,関数を値に適用した式や,定義の中に隠れている全称量化を見やすくする場面で使われます.
simp と違って,一般の補題による書き換えではなく,主に定義展開と計算による簡約を行います.
def FnUb (f : Nat → Nat) (a : Nat) : Prop :=
∀ x, f x ≤ a
example (f : Nat → Nat) (a : Nat) (h : FnUb f a) : f 0 ≤ a := by
dsimp [FnUb] at h
exact h 0
by を項の一部として使う¶
by は,証明全体だけでなく,項の一部としても使えます.
Lean がある場所で型 T の項を期待しているなら,その場所に by ... と書いて tactic で項を作れます.
たとえば P ∧ Q の証明は 2 つの成分を持つので,各成分の位置で小さな tactic 証明を書けます.
example (P Q : Prop) (hP : P) (hQ : Q) : P ∧ Q :=
⟨by
exact hP,
by
exact hQ⟩
example : {n : Nat // n < 3} :=
⟨2, by decide⟩
subtype の要素を作るには「値」と「その値が条件を満たす証明」が必要です.
上の例では,1 番目の成分が値 2,2 番目の成分が 2 < 3 の証明です.
後の章で出てくる ⟨2, by norm_num [ltThreeSet]⟩ も同じ形で,2 番目の成分だけを tactic で作っています.
名前つき定理でも,右辺に証明項を直接書けます. 短い証明では,この書き方の方が読みやすい場合があります.
tactic モードは「ゴールを変形する手続き」として読みやすく,証明項は「証明そのものを式として組み立てる」書き方です. 実際の開発では,短い証明は証明項で書き,長い証明や探索的な証明は tactic モードで書く,という使い分けがよくあります. ただしどちらの場合も,最終的には Lean のカーネルが証明項を型検査している点は同じです.
tactic と証明項の対応¶
tactic は証明を魔法のように作っているわけではありません. 各 tactic は,現在のゴールに対して,最終的には Lean のカーネルが検査できる証明項を構成します. Infoview ではゴールが変形されて見えますが,背後では「どのコンストラクタ,どの関数,どの定理を使って項を作るか」を指定していると考えるとよいです.
Chapter 02 で見た型の構成と対応させると,基本 tactic は次のように整理できます.
| ゴールまたは仮定の形 | 主な tactic | 対応する項・定義 |
|---|---|---|
P → Q,∀ x, P x を示す |
intro |
関数 fun h => ...,依存関数 |
P → Q,∀ x, P x を使う |
apply,関数適用 |
証明を関数として適用する |
P ∧ Q を示す |
constructor,exact ⟨_, _⟩ |
And.intro |
P ∧ Q を使う |
cases,rcases,obtain |
And.left,And.right,パターン分解 |
P ↔ Q を示す |
constructor |
Iff.intro |
P ↔ Q を使う |
rw,.mp,.mpr |
両方向の含意を field として使う |
P ∨ Q を示す |
left,right |
Or.inl,Or.inr |
P ∨ Q を使う |
match,cases,rcases |
Or のコンストラクタによる場合分け |
∃ x, P x を示す |
use,exact ⟨x, hx⟩ |
Exists.intro |
False から任意の命題を示す |
exfalso,contradiction |
False.elim |
a = b を示す |
rfl,既存補題 |
Eq.refl や Eq 型の証明 |
a = b を使う |
rw,subst |
等式による置換 |
inductive 型を使う |
match,cases,induction |
コンストラクタによる場合分け・帰納法 |
structure の等式を示す |
ext |
field ごとの等式 |
この対応を意識すると,tactic が失敗したときにも「いま必要なのは関数を作ることか,コンストラクタを使うことか,既存の等式で書き換えることか」を切り分けやすくなります. 以降の節では,この表の各行を具体例として見ていきます.
関数・含意・全称命題: intro,apply,specialize¶
含意 P → Q や全称命題 ∀ x, P x を示すときは,intro で仮定や変数を導入します.
ゴールが P → Q なら intro hP は hP : P をローカルコンテキストに追加し,ゴールを Q に変えます.
ゴールが ∀ x : α, P x なら intro x は任意の x : α を導入し,ゴールを P x に変えます.
example (P Q : Prop) (hQ : Q) : P → Q := by
intro _hP
exact hQ
example (P : Nat → Prop) (h : ∀ n : Nat, P n) : P 0 := by
exact h 0
apply は,現在のゴールを証明するために使えそうな定理や仮定を適用します.
ゴールが R で,hQR : Q → R があるとき,apply hQR により新しいゴールは Q になります.
より一般には,結論が現在のゴールと一致するような定理を後ろ向きに使い,その定理の前提を新しいゴールとして残します.
全称命題を具体的な値に適用したいときは,関数適用のように書けます.
specialize は,全称命題の仮定を特定の値に特殊化して,仮定そのものを書き換える tactic です.
次の例では,h : ∀ n, P n → Q n が specialize h 3 によって h : P 3 → Q 3 に変わります.
example (P Q : Nat → Prop) (h : ∀ n : Nat, P n → Q n) (hP : P 3) : Q 3 := by
specialize h 3
exact h hP
演習¶
intro と exact だけで証明してください.
余裕があれば,term-style proof でも同じ命題を書いてください.
途中結果: have と suffices¶
have は証明の途中で補題を作ります.
長い証明では,途中結果に名前をつけると読みやすくなります.
have h : Q := ... と書くと,以降の証明で h : Q を仮定として使えます.
show は現在のゴールを明示的に書き直すだけで,新しい仮定や途中結果は作りません.
それに対して,have は新しい証明項に名前をつけてローカルコンテキストへ追加します.
example (P Q R : Prop) (hPQ : P → Q) (hQR : Q → R) (hP : P) : R := by
have hQ : Q := hPQ hP
exact hQR hQ
suffices h : Q は,「Q が示せれば現在のゴールが従う」という形で証明を組み替えます.
先に最終段階を宣言し,あとで十分条件を証明する書き方です.
証明の流れとしては,まず Q から元のゴールを導く部分を書き,その後で Q 自体を証明します.
これも show とは異なり,現在のゴールを表示し直すのではなく,現在のゴールを「Q を示す」という新しいサブゴールに置き換えます.
example (P Q R : Prop) (hPQ : P → Q) (hQR : Q → R) (hP : P) : R := by
suffices hQ : Q from hQR hQ
exact hPQ hP
rw: 等式による書き換え¶
rw [h] は,等式 h : a = b を使って,ゴール中の a を b に書き換えます.
命題の中では,同値 P ↔ Q も書き換えに使えます.
rw は指定された補題を左から右へ使い,rw [← h] と書くと逆向きに使います.
書き換えの向きを逆にしたいときは ← を使います.
example (a b : Nat) : a + b = b + a := by
rw [Nat.add_comm]
example (a b : Nat) : b + a = a + b := by
rw [← Nat.add_comm a b]
仮定の中を書き換えるときは rw [h] at h2 のように at を使います.
congr は,両辺に同じ関数が適用されている等式を,引数の等式に帰着します.
たとえば f a = f b を示す問題を a = b に変えることができます.
次の例では,残った a = b のゴールをローカルコンテキストの h : a = b が閉じています.
演習¶
rw を使って等式を書き換えてください.
単純化: simp,simpa,<;>¶
simp は,定義展開,既知の単純化補題,仮定を使った書き換えを組み合わせて,ゴールを単純化します.
日常的に最もよく使う tactic の 1 つです.
simp は登録された [simp] 補題を,原則として式が単純になる向きに使います.
任意の定理を総当たりで使う tactic ではないので,使ってほしい定義や補題は simp [name] の形で明示します.
追加で使いたい定義や補題を simp [name] の形で渡せます.
def addZeroTwice (n : Nat) : Nat :=
n + 0 + 0
example (n : Nat) : addZeroTwice n = n := by
simp [addZeroTwice]
仮定の中を単純化するときは simp at h と書けます.
simp at * と書くと,ローカルコンテキストとゴール全体を対象にできますが,証明が読みにくくなる場合もあります.
simpa using h は,h の型と現在のゴールを単純化して一致させます.
最後の一手として非常に便利です.
内部的には「h を使う前後で simp する」と考えると読みやすいです.
simp_all は,ゴールとローカルコンテキストの仮定をまとめて単純化します.
仮定が多いときに有効です.
<;> は tactic を連結する notation です.
tac1 <;> tac2 と書くと,まず tac1 を実行し,その結果生じたすべてのゴールに tac2 を実行します.
同じ後処理を複数のゴールにまとめて適用したいときに使います.
example (P Q : Prop) (hP : P) (hQ : Q) : P ∧ Q := by
constructor <;> assumption
example (a b : Nat) : a + b = b + a ∧ a + 0 = a := by
constructor <;> simp [Nat.add_comm]
上の 1 つ目は,次の証明を短く書いたものです.
便利ですが,複雑な証明で乱用すると各ゴールに何が起きたか読みにくくなります.
最初は明示的に箇条書きで証明し,繰り返しが明らかなときだけ <;> でまとめるのが無難です.
演習¶
simp で証明できる命題を,まず手動で証明し,その後 simp や simpa で短くしてください.
calc モード¶
calc は,等式や不等式の連鎖を数学の計算のように書くための構文です.
各行の右側に,そのステップの根拠を書きます.
各ステップは,前の行の右辺と次の行の左辺をつなぐ証明になっている必要があります.
example (a b c : Nat) : (a + b) + c = b + (a + c) :=
calc
(a + b) + c = (b + a) + c := by
rw [Nat.add_comm a b]
_ = b + (a + c) := by
rw [Nat.add_assoc]
calc は等式だけでなく,推移律を持つ関係にも使えます.
次の例では ≤ の推移律を calc が使っています.
tactic モードの中で calc を使うこともできます.
example (a b c : Nat) : (a + b) + c = b + (a + c) := by
exact
calc
(a + b) + c = (b + a) + c := by
rw [Nat.add_comm a b]
_ = b + (a + c) := by
rw [Nat.add_assoc]
演習¶
calc モードで加法の結合律・可換律を使って証明してください.
example (a b c : Nat) : a + b + c = b + a + c := by
calc
a + b + c = b + a + c := by
-- `ac_rfl` または `rw [Nat.add_comm a b]` などを試す.
sorry
論理結合子とデータ構造: constructor,match,cases¶
連言 P ∧ Q や同値 P ↔ Q は structure として実装されていました.
これらを示すときは,constructor が対応する constructor を使い,必要な field をサブゴールとして生成します.
example (P Q : Prop) (hP : P) (hQ : Q) : P ∧ Q := by
constructor
· exact hP
· exact hQ
example (P Q : Prop) (hPQ : P → Q) (hQP : Q → P) : P ↔ Q := by
constructor
· intro hP
exact hPQ hP
· intro hQ
exact hQP hQ
連言や存在命題を分解するときは obtain が便利です.
obtain ⟨hP, hQ⟩ := h は,h をパターンに従って分解し,得られた成分に名前をつけます.
example (P Q : Prop) (h : P ∧ Q) : Q ∧ P := by
obtain ⟨hP, hQ⟩ := h
constructor
· exact hQ
· exact hP
example (P : Nat → Prop) (h : ∃ n : Nat, P n) : ∃ n : Nat, P n := by
obtain ⟨n, hn⟩ := h
exact ⟨n, hn⟩
rintro は intro とパターン分解を同時に行います.
含意の仮定を導入しながら,連言や存在命題をすぐに分解したいときに便利です.
match は,inductive 型の値をコンストラクタごとに分けて式を作る構文です.
命題の証明でも,証明項を直接書くときにはよく現れます.
次の例では,h : P ∨ Q が Or.inl hP で作られている場合と Or.inr hQ で作られている場合に分けています.
example (P Q R : Prop) (h : P ∨ Q) (hPR : P → R) (hQR : Q → R) : R :=
match h with
| Or.inl hP => hPR hP
| Or.inr hQ => hQR hQ
自然数も inductive 型なので,match で Nat.zero と Nat.succ k の場合に分けられます.
cases は,このコンストラクタごとの分解を tactic モードで行うものだと見ると分かりやすいです.
example (n : Nat) : n = 0 ∨ ∃ k : Nat, n = k + 1 :=
match n with
| Nat.zero => Or.inl rfl
| Nat.succ k => Or.inr ⟨k, rfl⟩
cases は,手元にある値や証明を,その型のコンストラクタごとに分けて使う tactic です.
基本構文は次の形です.
ここで h は inductive 型の値や証明です.
cases h with と書くと,Lean は h の型を見て,その型を作る各コンストラクタに対応する枝を作ります.
たとえば P ∨ Q は Or という inductive 型で,コンストラクタ Or.inl : P → P ∨ Q と Or.inr : Q → P ∨ Q を持ちます.
そのため,h : P ∨ Q に対して cases h with を使うと,P の証明がある枝と Q の証明がある枝に分かれます.
example (P Q R : Prop) (h : P ∨ Q) (hPR : P → R) (hQR : Q → R) : R := by
cases h with
| inl hP =>
exact hPR hP
| inr hQ =>
exact hQR hQ
cases は命題だけでなく,普通の帰納型の値にも使えます.
Nat は zero と succ というコンストラクタを持つ inductive 型なので,cases n with は
n = 0 の枝と n = k + 1 の枝を作ります.
重要なのは,cases が「任意の命題について真偽を場合分けする」tactic ではなく,
すでにある値や証明をその帰納型のコンストラクタに従って分解する tactic だという点です.
一般の命題 P : Prop について P と ¬ P に分けるには,後で扱う by_cases h : P を使います.
example (n : Nat) : n = 0 ∨ ∃ k : Nat, n = k + 1 := by
cases n with
| zero =>
exact Or.inl rfl
| succ k =>
exact Or.inr ⟨k, rfl⟩
rcases は,仮定をパターンに従って分解します.
cases よりも複雑な入れ子の分解を短く書けます.
example (P Q R : Prop) (h : (P ∧ Q) ∨ R) : Q ∨ R := by
rcases h with ⟨_hP, hQ⟩ | hR
· left
exact hQ
· right
exact hR
存在命題を作るには,証拠とその証明を与えます.
Mathlib などを読み込んだ環境では,use という tactic で証拠を指定することもよくあります.
ここでは Core Lean だけでも使えるように,Exists.intro の notation である ⟨3, rfl⟩ を直接書きます.
選言 P ∨ Q を示すには,left または right でどちらを示すかを選びます.
example (P Q : Prop) (hP : P) : P ∨ Q := by
left
exact hP
example (P Q : Prop) (hQ : Q) : P ∨ Q := by
right
exact hQ
演習¶
constructor と cases を使って,連言の順序を入れ替えてください.
constructor は And.intro に対応し,cases は And の証明から左右の成分を取り出す操作に対応します.
帰納型と帰納法: induction¶
自然数やリストのような inductive 型について証明するときは,induction を使います.
自然数 n : Nat に対する帰納法では,zero の場合と succ n の場合を証明します.
cases が単なる場合分けであるのに対して,induction は再帰的なコンストラクタの枝で帰納法の仮定を生成します.
theorem nat_add_assoc_by_induction (a b c : Nat) : (a + b) + c = a + (b + c) := by
induction a with
| zero =>
simp
| succ a ih =>
simp [Nat.succ_add, ih]
帰納法の帰納ステップでは,ih が帰納法の仮定です.
上の例では,ih : (a + b) + c = a + (b + c) を使って,Nat.succ a の場合を示しています.
theorem list_length_map_by_induction {α β : Type} (f : α → β) (xs : List α) :
(xs.map f).length = xs.length := by
induction xs with
| nil =>
rfl
| cons x xs ih =>
simp [ih]
自分で定義した inductive 型に対しても cases や induction を使えます.
次の Even は「偶数である」という命題を帰納的に定義したものです.
Even n は n が偶数であることを表す命題であり,その証明は zero と add_two から作られます.
inductive Even : Nat → Prop where
| zero : Even 0
| add_two {n : Nat} : Even n → Even (n + 2)
example : Even 4 := by
apply Even.add_two
apply Even.add_two
exact Even.zero
example (h : Even 1) : False := by
cases h
帰納的に定義された命題の証明 h : Even n に対しても induction h が使えます.
これは「その証明がどのコンストラクタで作られたか」に関する帰納法です.
cases h は不可能なコンストラクタの枝を自動的に消すため,Even 1 からは矛盾が得られます.
theorem even_plus_two_of_even {n : Nat} (h : Even n) : Even (n + 2) := by
exact Even.add_two h
example (n : Nat) (h : Even n) : Even (n + 2) := by
induction h with
| zero =>
exact Even.add_two Even.zero
| add_two h ih =>
exact Even.add_two ih
演習¶
induction で自然数の加法単位元を証明してください.
関数と structure: funext と ext¶
関数の等式は,すべての入力で値が等しいことを示せば証明できます.
この原理を関数外延性と呼び,Lean では funext を使います.
structure についても,field ごとの等式から structure 全体の等式を示すことがあります.
@[ext] を付けておくと,ext tactic が使う外延性補題が生成されます.
@[ext]
structure PointForExt where
x : Nat
y : Nat
example (p q : PointForExt) (hx : p.x = q.x) (hy : p.y = q.y) : p = q := by
ext
· exact hx
· exact hy
演習¶
funext を使って,点ごとの等式から関数の等式を証明してください.
矛盾と背理法¶
by_cases h : P は,命題 P が成り立つ場合と成り立たない場合に分けます.
一般の命題 P : Prop に対する場合分けは,古典論理の排中律に依存します.
P が計算で判定可能な命題なら,その判定手続きに基づく場合分けとしても読めます.
Classical.byContradiction は背理法です.
¬ P → False から P を結論します.
これは一般には古典論理の原理です.
構成的に証明したい場面では,¬ P を仮定して False を示す「否定の証明」と区別して使います.
否定が量化子の外側にある命題を扱うときも,Core Lean だけで証明できます.
たとえば ¬ ∀ n, P n から ∃ n, ¬ P n を得るには,古典論理の背理法を使います.
Mathlib では push Not がこの種の変形を自動化してくれますが,ここでは明示的に証明します.
example (P : Nat → Prop) (h : ¬ ∀ n, P n) : ∃ n, ¬ P n := by
exact Classical.byContradiction (fun hNoExists =>
h (fun n =>
Classical.byContradiction (fun hn =>
hNoExists ⟨n, hn⟩)))
exfalso は,現在のゴールを False に変えます.
矛盾を導けば任意のゴールが閉じる,という規則を使うための tactic です.
False.elim を tactic モードで使いやすくしたものと考えるとよいです.
contradiction は,コンテキストにある矛盾を探してゴールを閉じます.
汎用的な tactic¶
decide¶
Lean が真偽を計算できる命題を決定して証明します.
有限な計算で判定できる命題に有効です.
対象の命題に対する Decidable インスタンスがあり,計算結果が真である場合にゴールを閉じます.
grind¶
書き換え,前向き推論,後ろ向き推論,場合分けなどを組み合わせる汎用自動化 tactic です. 強力ですが,何をしたのかが見えにくくなることもあるので,講義資料では短い例に限定して使います. 自動化 tactic は証明を短くしますが,初学段階ではまず手動の tactic でゴールの変化を追えるようにしておくことが重要です.
example (P Q R : Prop) (hPQ : P → Q) (hQR : Q → R) (hP : P) : R := by
grind
example (P Q : Prop) (h : P ∧ Q) : Q ∧ P := by
grind
simp や grind は便利ですが,証明が通らないときに原因を理解しにくいことがあります.
最初は intro,apply,constructor,cases,rw などで証明構造を書けるようにしてから,自動化を使うのがよいです.
演習¶
grind で閉じる論理問題を作り,手動証明と比較してください.
検索系: #check,exact?,rw?¶
使える補題を探すことは,Lean で数学を形式化するときの大きな作業です.
#check¶
名前が分かっている定理の型を確認します.
定理名が分かっている場合は #check,現在のゴールから候補を探したい場合は exact?,rw? のような検索支援を使い分けます.
exact?,rw?,try?¶
現在のゴールを閉じる候補や書き換え候補を提案する tactic です. 出力は Lean のバージョンや読み込んだライブラリによって変わることがあるため,この資料では実行例としてだけ示します.
example (n : Nat) : n + 0 = n := by
exact?
example (a b : Nat) : a + b = b + a := by
rw?
example (P Q : Prop) (h : P ∧ Q) : Q ∧ P := by
try?
Mathlib には #loogle などのより強力な検索支援もあります.
それらは次章以降,Mathlib を使う場面で扱います.
conv モード¶
conv モードは,ゴール全体ではなく,式の特定の部分に入り込んで書き換えるためのモードです.
通常の rw はゴール全体から書き換え場所を探します.
一方,conv では「左辺に入る」「第 1 引数に入る」のように場所を指定してから書き換えます.
conv => の中では lhs や rhs を使って,左辺・右辺を選べます.
上の例では,左辺 (a + b) + c に入り,さらに第 1 引数 a + b に入ってから,そこだけを交換しています.
arg 1 は関数適用や演算の第 1 引数へ移動する指示です.
conv の中でも simp を使えます.
式の一部だけを簡約したいときに便利です.
conv は強力ですが,構文が細かく,証明が読みにくくなることもあります.
まずは通常の rw や simp を試し,書き換える場所を厳密に指定したいときに conv を使うのがよいです.
演習¶
conv を使って,ゴールの一部だけを書き換えてください.
example (a b c : Nat) : (a + b) + c = (b + a) + c := by
-- ヒント:
-- conv =>
-- lhs
-- rw [Nat.add_comm a b]
sorry
Lean における等号の証明パターン¶
Lean の等号 a = b は Eq a b という型です.
つまり「a = b を証明する」とは,型 Eq a b の証明項を構成することです.
ただし,数学で一言で「等しい」と言うものが,Lean ではいくつかの層に分かれます.
| レベル | 数学での状況 | Lean での姿 | 典型的な証明方法 |
|---|---|---|---|
| 定義的な等しさ | 定義から同じ | definitional equality | rfl,change,dsimp |
| 命題的な等しさ | 補題や仮定で等しい | propositional equality, Eq |
rw,calc,exact h |
| 自動化された等式証明 | 正規化すれば同じ | Eq の証明を tactic が作る |
simp,decide,grind |
| 外延的な等しさ | 点ごと・元ごと・成分ごとに同じ | extensional equality | funext,ext |
| 命題外延性 | 論理的に同値な命題を等しい命題として扱う | propositional extensionality | propext |
| 商での等しさ | 同値な代表元を商では同じと見る | quotient equality | Quot.sound |
この表のうち,最初の definitional equality だけは Lean の項として直接書く命題ではありません.
これは Lean のカーネルが内部で判断する「計算と定義展開により同じ式である」という関係です.
両辺が定義的に等しければ,rfl によって命題的等号 a = b の証明を作れます.
逆に,h : a = b があるからといって,a と b が定義的に等しいとは限りません.
この環境の自然数の加法では,n + 0 は定義を展開すると n になり,rfl で閉じます.
一方,0 + n = n は数学的には同じくらい基本的な等式ですが,変数 n が具体的に 0 か Nat.succ k か分からない状態では計算が進みません.
したがって,これは一般には definitional equality ではなく,定理として証明する propositional equality です.
simp で閉じるからといって,その等式が definitional equality であるとは限りません.
simp は定義展開だけでなく,Nat.zero_add のような単純化補題を使って Eq の証明を作ります.
decide や grind も同様に,判定手続きや推論によって証明を生成する tactic であり,単に rfl で閉じているわけではない場合があります.
交換法則のような等式も,数学的には明らかでも定義を展開するだけでは同じ式になりません.
この場合は,Nat.add_comm のような定理を使って Eq 型の証明項を構成します.
example (a b : Nat) : a + b = b + a := by
exact Nat.add_comm a b
example (a b c : Nat) : a + (b + c) = b + (a + c) := by
calc
a + (b + c) = (a + b) + c := by
rw [Nat.add_assoc]
_ = (b + a) + c := by
rw [Nat.add_comm a b]
_ = b + (a + c) := by
rw [Nat.add_assoc]
等式は書き換えにも使います.
rw [h] は,証明項 h : a = b を使って,ゴール中の a を b に置き換えます.
example (a b : Nat) (h : a = b) : a.succ = b.succ := by
rw [h]
example (a b c : Nat) (h : a = b) : a + c = b + c := by
rw [h]
example (a b c : Nat) (h : a = b) : b + c = a + c := by
rw [← h]
同じことを,合同性の補題 congrArg を使って証明することもできます.
これは「等しい入力に同じ関数を適用すれば,結果も等しい」という原理です.
少し先の話ですが,型の等号と同型・同値も区別します.
数学では「同型な対象を同じと思う」と言うことがありますが,Lean では α = β と α ≃ β は別の主張です.
等号 h : α = β があれば cast h により項を移送できますが,これは同値や同型を与えることとは違います.
定義的な等しさ由来の cast は計算で消えやすく,funext や ext から来る非自明な等号は,証明上は便利でも計算上は扱いが重くなることがあります.
命題外延性 propext や商型 Quot による等号は,次の advanced 節で扱います.
ここで重要なのは,すべての「等しい」が同じ理由で証明されるわけではない,という点です.
rfl は Lean が計算で同じだと分かる等号です.
rw,simp,calc は,補題や仮定から Eq の証明を作って使います.
funext や ext は,点ごと・元ごと・成分ごとの一致から全体の等式を作ります.
propext や Quot.sound は,標準的な数学を Lean で扱うための追加原理と結びついています.
証明が通らないときは,まず「これは定義的な等しさで閉じたいのか,補題による等式なのか,外延性で成分ごとに示すべき等式なのか,advanced な原理に由来する等式なのか」を切り分けると,次に使う tactic が選びやすくなります.
古典論理,選択,商,Lean の追加原理¶
ここまで見た基本的な証明項の構成は,かなり構成的な性質を持っています.
つまり,一般の命題 P : Prop について P ∨ ¬ P を証明するには,P の証明か ¬ P の証明のどちらかを実際に与える必要があります.
排中律や背理法,存在からの非構成的な選択を自由に使う標準的な数学を扱うには,追加の原理が必要になります.
Lean の標準的な基礎には,命題外延性,商,選択などの原理が用意されています. これは Mathlib 独自の仮定ではなく,Lean の上で通常の数学を展開するための基礎です. Mathlib はその上に多くの定義と定理を積み上げています. これらを使う場合も,Lean は最終的にその原理を含む証明項を構成し,カーネルが型を確認します.
参考:
- Theorem Proving in Lean 4, Axioms and Computation: https://leanprover.github.io/theorem_proving_in_lean4/Axioms-and-Computation/#axioms-and-computation
- nLab, choice operator: https://ncatlab.org/nlab/show/choice+operator
排中律は Classical.em P として使えます.
実際の証明では,前に見た by_cases h : P や Classical.byContradiction を通じて使うことが多いです.
ここでは,「標準的な数学をするために,Lean にはこのような原理が用意されている」という位置づけを押さえておきます.
命題外延性 propext は,論理的に同値な命題を等しい命題として扱うための原理です.
P ↔ Q は「互いに導ける」という命題であり,P = Q は Prop の元としての命題そのものの等号です.
この 2 つは区別されますが,propext により P ↔ Q から P = Q を作れます.
#check propext
example (P Q : Prop) (h : P = Q) : P ↔ Q := by
rw [h]
example (P Q : Prop) (h : P ↔ Q) : P = Q := by
exact propext h
example (P Q : Prop) : (P ∧ Q) = (Q ∧ P) := by
apply propext
constructor
· intro h
exact ⟨h.2, h.1⟩
· intro h
exact ⟨h.2, h.1⟩
商型では,同値関係で同値な代表元を,同じ商の元として扱います.
次の例では,自然数を「2 で割った余りが等しい」という関係で割った商を考えています.
0 と 2 は代表元としては異なりますが,この商の中では等しく,その等式は Quot.sound で作られます.
def SameModTwo (a b : Nat) : Prop :=
a % 2 = b % 2
example : Quot.mk SameModTwo 0 = Quot.mk SameModTwo 2 := by
exact Quot.sound (by rfl)
選択は,存在命題から証拠を選ぶときに現れます.
数学では「条件を満たすものを 1 つ取る」と自然に書きますが,Lean でその選んだ値を定義として保存するには Classical.choose を使います.
このような定義は一般に計算可能な内容を持たないため,noncomputable として宣言します.
ここでいう「選択」は,集合論の選択公理をそのまま Lean に書き写したものではありません.
集合論の選択公理は,非空集合の族から選択関数が存在する,という集合論内部の命題です.
一方,Lean の型理論で使う Classical.choice は,型 α が空でないという命題 Nonempty α の証明から,実際に α の項を 1 つ返す選択演算子です.
つまり Classical.choice : Nonempty α → α は,証明情報からデータを取り出す追加原理です.
各添字 i ごとにこれを使えば,集合論の選択公理に似た選択関数を作れます.
ただし,この関数は計算規則を持たないため,値を定義として保存する場合は noncomputable になります.
noncomputable def choiceFunctionFromNonempty {ι : Type} {α : ι → Type}
(h : ∀ i : ι, Nonempty (α i)) : (i : ι) → α i :=
fun i => Classical.choice (h i)
example {ι : Type} {α : ι → Type} (h : ∀ i : ι, Nonempty (α i)) :
Nonempty ((i : ι) → α i) := by
exact ⟨choiceFunctionFromNonempty h⟩
noncomputable def chosenNatFromExistence (h : ∃ n : Nat, 10 < n) : Nat :=
Classical.choose h
example (h : ∃ n : Nat, 10 < n) : 10 < chosenNatFromExistence h := by
exact Classical.choose_spec h
命題外延性 propext は,論理的に同値な命題を等しい命題として扱うために使います.
商型は,同値関係で割った対象を作るために使います.
たとえば整数,有理数,剰余類,商群など,標準的な数学では「同値なものを同一視する」構成が頻繁に現れます.
この章で押さえておきたいのは,これらが tactic の小技ではなく,Lean で標準的な数学を表現するための基礎的な仕組みだという点です. 一方で,証明項を構成しているという基本的な見方は変わりません. 古典論理や選択を使う場合も,Lean はその原理を使った証明項を構成し,カーネルが型を確認しています.
演習¶
propext を使って,論理的に同値な命題を等しい命題として扱ってください.
Classical.choose と Classical.choose_spec を使って,存在命題から証拠を取り出してください.
noncomputable def chosenNatExercise03 (h : ∃ n : Nat, n > 10) : Nat :=
Classical.choose h
example (h : ∃ n : Nat, n > 10) : chosenNatExercise03 h > 10 := by
-- `Classical.choose_spec h`
sorry
Quot.sound を使って,商型の中で代表元が等しいことを証明してください.
def sameModTwoExercise03 (a b : Nat) : Prop :=
a % 2 = b % 2
example : Quot.mk sameModTwoExercise03 1 = Quot.mk sameModTwoExercise03 3 := by
-- `Quot.sound` と `decide`
sorry
まとめ¶
tactic モードでは,現在のゴールとローカルコンテキストを見ながら,証明を小さなステップに分解します.
intro と apply は含意や全称命題,rw,simp,calc は等式や計算,constructor,match,cases,induction は structure や inductive 型に対応します.
外延性を使う funext や ext,矛盾を扱う exfalso,contradiction,背理法は数学でよく使うため,基本 tactic の次に押さえておくと便利です.
conv,等号パターンの総整理,古典論理・選択・商などの追加原理は,証明が複雑になったときに参照する後半の話題として位置づけています.
Chapter 04: Mathlib を用いた証明の書き方¶
この章では,Mathlib を使って証明を書くときの基本的な読み方・書き方を整理します. 線形代数や微積分の各論は実践編で扱い,ここではそれらに共通する Mathlib の基本的な使い方を見ます.
Mathlib は,Lean の上で通常の数学を形式化するための大規模なライブラリです. 群・環・体・位相空間・測度・多様体などの定義,それらに関する定理,notation,型クラスインスタンス,証明を補助する tactic が含まれています. Lean 本体の小さなカーネルが証明の正しさを検査し,Mathlib はその上に数学の語彙と既存定理を積み上げている,と考えるとよいです. したがって,Mathlib を import しても「証明を信用で通す」わけではなく,最終的な証明項は Lean のカーネルで型検査されます. 前章では,証明とは証明項を構成することであり,tactic はその証明項を組み立てるための書き方だと見ました. この章では tactic そのものの基本説明は繰り返しすぎず,Mathlib の定義・定理・型クラスインスタンスをどう読み,既存の証明項としてどう使うかに重心を置きます.
この章では,まず Mathlib の定義や定理を読むための一般的な道具を確認し,次に Set,Finset,実数系の型,不等式という具体的な対象を見ます.
最後に,Mathlib で証明を書くときによく使う tactic や検索の考え方を整理します.
特に次の内容を扱います.
import Mathlibと名前空間・スコープつき notation- 型クラスで一般化された定理の読み方
- bundled structure,morphism,subobject,coercion
Set,Finset,Real,NNReal,EReal,ENNReal- 不等式の型と証明パターン
- Mathlib でよく使う tactic と検索支援
- Mathlib の命名規則
なお,Set α,Finset α,Real,NNReal,EReal,ENNReal は「クラス」ではなく型です.
ただし,それらの型には LinearOrder,TopologicalSpace,CompleteLinearOrder などの型クラスインスタンスが登録されており,そのインスタンスによって一般定理を適用したり,tactic が必要な構造を見つけたりできるようになります.
Mathlib を読むときは,「対象そのものの型」と「その型に入っている構造」を分けて見ることが重要です.
参考:
- Mathematics in Lean: https://leanprover-community.github.io/mathematics_in_lean/index.html
- Mathlib overview: https://leanprover-community.github.io/mathlib-overview.html
- Mathlib naming conventions: https://leanprover-community.github.io/contribute/naming.html
- Lean community terminology: https://leanprover-community.github.io/glossary.html
import Mathlib と open scoped¶
このプロジェクトでは各章の冒頭で import Mathlib を使っています.これは Mathlib 全体を読み込みます.
実際の開発では,ビルド時間を抑えるために必要なファイルだけを import することもあります.
たとえば import Mathlib.Data.Real.Basic や import Mathlib.Topology.Basic のように,必要な分野のファイルだけを import する書き方です.
import Mathlib は「Mathlib に含まれる主要な定義・定理・tactic をまとめて使えるようにする」指定であり,Lean の言語機能そのものとは区別します.
一部の notation は,スコープを開かないと使えません.
有限和 ∑ には BigOperators,拡張非負実数の ∞ には ENNReal のスコープを使います.
open scoped は名前空間の中の定義名を短くするというより,特定の notation やスコープ付き notation を有効にするために使います.
Mathlib では,定義そのものと notation が分かれていることがよくあります.
たとえば Real という型には ℝ という notation があり,NNReal には ℝ≥0,ENNReal には ℝ≥0∞ という notation があります.
また ∑ i ∈ s, f i は有限和を表す notation で,背後には Finset.sum があります.
証明中で notation の意味が分からなくなったら,#check で型を確認し,必要なら対応する定義名を探します.
#check は,Mathlib の名前がどのような型をもつかを確認する基本コマンドです.
#synth は,指定した型クラスインスタンスを Lean が見つけられるかを確認します.
#check Finset
#check Set
#check Real
#check NNReal
#check EReal
#check ENNReal
#check ℝ
#check ℝ≥0
#check ℝ≥0∞
#check (∞ : ℝ≥0∞)
#synth Field ℝ
#synth LinearOrder ℝ
#synth TopologicalSpace ℝ
#synth Add ENNReal
#synth CompleteLinearOrder ENNReal
演習¶
#synth で,実数が体・線形順序・狭義順序環の構造を持つことを確認してください.
型クラスで一般化された定理を読む¶
Mathlib の定理は,特定の型だけではなく,型クラスで一般化された形で書かれることがよくあります.
たとえば add_comm は自然数専用の定理ではなく,足し算が可換な任意の型で使える定理です.
#check add_comm の結果を見ると,必要な型クラス仮定が明示されます.
#check add_comm
#check add_zero
#check le_total
section GenericAlgebra
variable {α : Type*} [AddCommMonoid α]
example (a b : α) : a + b = b + a := by
exact add_comm a b
example (a : α) : a + 0 = a := by
exact add_zero a
end GenericAlgebra
角括弧 [AddCommMonoid α] は,「α には加法可換モノイド構造がある」というインスタンスを要求します.
Lean はこの仮定を使って + や 0 の意味を決め,add_comm や add_zero を適用します.
実際には add_comm だけならより弱い AddCommMagma で十分ですが,ここでは add_zero も同じ section で使うため AddCommMonoid を仮定しています.
Type* は universe レベルを Lean に推論させる notation で,Type u の universe 変数を明示しない書き方です.
Chapter 02 で見た LE や Add と同じく,Mathlib の AddCommMonoid や LinearOrder も型クラスです.
違いは,Mathlib ではそれらが階層化され,多くの演算・法則・notation・補題をまとめて利用できるようになっている点です.
section GenericOrder
variable {α : Type*} [LinearOrder α]
example (a b : α) : a ≤ b ∨ b ≤ a := by
exact le_total a b
end GenericOrder
このように,Mathlib の多くの定理は「型 α と,その型に入っている構造」を分けて書きます.
後半の章で現れる Group,Ring,TopologicalSpace,Module なども同じ発想です.
bundled structure,morphism,subobject¶
Mathlib では,数学的対象を structure として束ねることがよくあります.
たとえば準同型は,単なる関数ではなく「関数本体」と「演算を保つ証明」を持つ structure です.
これを bundled structure と呼ぶことがあります.
bundled にすると,写像そのものと構造保存の証明を 1 つの対象として渡せるため,定理の仮定や合成の記述が整理されます.
#check MonoidHom
#check RingHom
#check MonoidHom.map_one
#check map_add
example : (RingHom.id ℤ) 3 = 3 := by
rfl
example (f : ℤ →+* ℤ) (x y : ℤ) : f (x + y) = f x + f y := by
exact map_add f x y
ℤ →+* ℤ は,整数環から整数環への環準同型です.
map_add は,加法を保つことを表す典型的な補題名です.
Mathlib では,map_zero,map_one,map_mul,map_add のように,写像が構造を保つ定理が統一的な名前で用意されています.
準同型 f : ℤ →+* ℤ は関数のように f x と適用できますが,内部的には関数だけでなく保存性の証明も持っています.
部分構造も bundled な対象として扱われます.
たとえば線形代数では Submodule R M,代数では Subgroup G や Subsemiring R,位相では部分空間に関する構造が現れます.
各論は次章以降で扱いますが,ここでは名前だけ確認しておきます.
coercion と subtype¶
Mathlib では,自然な埋め込みは coercion として登録されています.
たとえば自然数を実数として使うとき,((3 : Nat) : ℝ) のように型を変換します.
coercion は便利ですが,エラーメッセージを読むときには「Lean がどの型からどの型へ自動変換したのか」を意識する必要があります.
Subtype は,条件を満たす要素を集めた型です.
{n : Nat // 0 < n} は,正の自然数とその証明を組にした型です.
値を元の型に戻すときは coercion が働きます.
x.property は,x が subtype の条件を満たすことの証明です.
ここまでが,Mathlib の定義や定理を読むための一般的な見方です. 以下では,数学で頻繁に使う具体的な型を順に見ます.
Set: 型の上の集合¶
Lean は型理論を基礎にしているため,「集合」は型として定義されます.
たとえば自然数全体は Nat,実数全体は ℝ という型です.
Set α は「型 α の部分集合」の型を表します.
定義は単純で,Set α は α → Prop として実装されています.
つまり,s : Set α とは,各 x : α に対して「x が s に属する」という命題を返す述語です.
所属 x ∈ s は,内部的には s x と同じ意味です.
この意味で,Lean の Set α は固定された型 α 上の部分集合全体,つまり冪集合 \(\mathcal P(\alpha)\) に対応します.
ただし実際の証明では,Set が関数として実装されていることを直接展開するより,{x | p x} や x ∈ s や s ⊆ t のような集合のインターフェースを使うのが普通です.
対応を大まかに書くと,次のようになります.
- 全体集合・台集合 \(\alpha\): Lean では多くの場合
α : Type* - 部分集合 \(s \subseteq \alpha\): Lean では
s : Set α - 冪集合 \(\mathcal P(\alpha)\): Lean では
Set αという型 - 条件 \(x \in s\): Lean では命題
x ∈ sまたはs x
Set の notation の定義を読む¶
Set の記法は,主に次の定義に対応します.
x ∈ s は Set.Mem s x,つまり s x です.
{x | p x} は述語 p から集合を作る setOf の記法です.
s ⊆ t は,s の任意の元が t にも入るという命題です.
def Set (α : Type u) := α → Prop
def setOf {α : Type u} (p : α → Prop) : Set α :=
p
namespace Set
protected def Mem (s : Set α) (a : α) : Prop :=
s a
instance : Membership α (Set α) :=
⟨Set.Mem⟩
protected def Subset (s₁ s₂ : Set α) :=
∀ ⦃a⦄, a ∈ s₁ → a ∈ s₂
instance : LE (Set α) :=
⟨Set.Subset⟩
instance : HasSubset (Set α) :=
⟨(· ≤ ·)⟩
#check Set
#check Set.powerset
#check Set.ext
#check Set.mem_setOf
#check Set.setOf_bijective
def ltThreeSet : Set Nat :=
{n | n < 3}
example (n : Nat) : n ∈ ltThreeSet ↔ n < 3 := by
rfl
example : 2 ∈ ltThreeSet := by
norm_num [ltThreeSet]
example : 4 ∉ ltThreeSet := by
norm_num [ltThreeSet]
example (s t : Set Nat) : t ∈ Set.powerset s ↔ t ⊆ s := by
exact Set.mem_powerset_iff t s
集合の等式は,通常,外延性で証明します.
数学で「任意の x について x ∈ A と x ∈ B が同値だから A = B」と言う議論に対応するのが Set.ext や tactic の ext です.
Set α と似て見えるものに Subtype があります.
s : Set α は α 上の述語であり,それ自体は Set α 型の項です.
一方,{x : α // x ∈ s} は,s に属する元だけを集めた新しい型です.
要素は値 x : α と証明 x ∈ s の組です.
Mathlib では s : Set α を型として使うと,この subtype へ coercion されます.
たとえば x : ltThreeSubtype は「ltThreeSet の元」としてのデータであり,(x : Nat) に戻すと元の自然数が得られます.
abbrev ltThreeSubtype : Type :=
{n : Nat // n ∈ ltThreeSet}
#check Subtype
#check Set.coe_eq_subtype
#check Subtype.mem
example : (⟨2, by norm_num [ltThreeSet]⟩ : ltThreeSubtype).1 = 2 := by
rfl
example (x : ltThreeSubtype) : (x : Nat) ∈ ltThreeSet := by
exact x.property
演習¶
Set が部分集合として振る舞うことを,membership と外延性で確認してください.
example (n : Nat) : n ∈ ltThreeSet ↔ n < 3 := by
-- 解答例:
-- rfl
sorry
example (s t : Set Nat) : t ∈ Set.powerset s ↔ t ⊆ s := by
-- 解答例:
-- exact Set.mem_powerset_iff t s
sorry
Set.ext を使って集合の等式を証明してください.
example {α : Type} (s t : Set α) : s ∩ t = t ∩ s := by
-- `ext x`, `constructor`, `intro h` で進める.
-- 解答例:
-- ext x
-- constructor
-- · intro h
-- exact ⟨h.2, h.1⟩
-- · intro h
-- exact ⟨h.2, h.1⟩
sorry
Finset: 有限集合¶
Finset α は,型 α の元からなる有限集合です.
リストとは違い,重複を持たない集合として扱います.
一方で,有限集合なので card,有限和 ∑,有限積 ∏ などを使えます.
Finset α は Set α に有限性を付けたものというより,有限個の要素をデータとして持つ型です.
そのため,計算や有限和・有限積との相性がよい一方,一般の集合論的な部分集合は Set α で表します.
Mathlib では,有限な対象や集合的な対象を表す方法がいくつかあります.
List α: 順序と重複を持つ有限列です.計算や再帰に向いています.Multiset α: 順序を無視し,重複は残す有限多重集合です.Set α:α → Propとして実装される,型αの部分集合です.有限とは限りません.Finset α: 順序も重複も無視する有限集合です.有限和や有限積の添字に向いています.Fintype α: 型αのすべての元が有限個である,という型クラスです.Subtype: 条件を満たす元を新しい型として扱う方法です.
したがって,順序つきのデータとして扱いたいなら List,重複個数を気にするなら Multiset,有限集合として和や濃度を扱いたいなら Finset,型 α の一般の部分集合なら Set α を使います.
多くの操作では,元の等号を判定できること,つまり [DecidableEq α] が必要になります.
自然数 Nat にはそのインスタンスがあるため,次の例はそのまま動きます.
たとえば,要素を挿入したときに既に含まれているかどうかを判定するには,要素同士の等号を決定できる必要があります.
#check List
#check Multiset
#check Finset
#check Fintype
def finsetSectionList : List Nat :=
[1, 2, 1]
#eval finsetSectionList
def finsetSectionMultiset : Multiset Nat :=
finsetSectionList
#eval finsetSectionMultiset
def finsetSectionFinset : Finset Nat :=
{1, 1, 2}
#eval finsetSectionFinset
#eval finsetSectionFinset.card
List は順序と重複をそのまま持つため [1, 2, 1] と表示されます.
Multiset は順序を無視しますが,重複は残します.
Finset は重複を消すので,{1, 1, 2} から作っても中身は {1, 2} になります.
Set は述語なので,それ自体を全要素のリストとして表示することはできません.
定義を見たいときは #print,有限範囲で様子を見たいときは Finset.range と filter を組み合わせるとよいです.
def finsetSectionSet : Set Nat :=
{n | n < 3}
#print finsetSectionSet
#check ((2 : Nat) ∈ finsetSectionSet)
def finsetSectionSetWindow : Finset Nat :=
(Finset.range 6).filter (fun n => n < 3)
#eval finsetSectionSetWindow
Finset.range n は {0, 1, ..., n - 1} です.
decide を #eval すると,具体的な membership の真偽を Bool として見られます.
有限和は ∑ i ∈ s, f i の形で書けます.
def finsetSectionRange : Finset Nat :=
Finset.range 5
#eval finsetSectionRange
#eval decide ((3 : Nat) ∈ finsetSectionRange)
#eval decide ((5 : Nat) ∈ finsetSectionRange)
def finsetSectionRangeSum : Nat :=
∑ i ∈ Finset.range 4, i
#eval finsetSectionRangeSum
Fintype α は「型 α 全体が有限である」という型クラスです.
たとえば Fin 3 は 3 個の元を持つ型で,Finset.univ はその型の全要素からなる有限集合です.
Finset α は「有限個の α の要素を集めたデータ」であり,Fintype α は「型 α 自身が有限である」という構造です.
したがって Fintype そのものをデータとして表示するより,その型の全要素を Finset.univ として取り出して表示します.
def finsetSectionFintypeData : Finset (Fin 3) :=
Finset.univ
#eval finsetSectionFintypeData
#eval Fintype.card (Fin 3)
Subtype は条件を満たす元を「値と証明の組」として持つ型です.
表示するときは,元の型への coercion を使うと値の部分を見られます.
def finsetSectionSubtypeElem : ltThreeSubtype :=
⟨2, by norm_num [ltThreeSet]⟩
#eval (finsetSectionSubtypeElem : Nat)
#check finsetSectionSubtypeElem.property
Finset と Set は相互に関係しますが,同じものではありません.
Finset は有限性をデータとして持つため,和や積を直接定義できます.
Set α は単に α → Prop なので,有限とは限らず,有限和には追加の有限性情報が必要です.
演習¶
Finset.range のデータを作り,membership と有限和を #eval で確認してください.
def exerciseRange : Finset Nat :=
Finset.range 5
#eval exerciseRange
#eval decide ((3 : Nat) ∈ exerciseRange)
#eval decide ((5 : Nat) ∈ exerciseRange)
def exerciseRangeSum : Nat :=
∑ i ∈ Finset.range 4, i
#eval exerciseRangeSum
Real, NNReal, EReal, ENNReal: 実数系の型¶
数学では実数の構成として,Dedekind cut,Cauchy 列による完備化,完備順序体としての公理化など,いくつかの標準的な方法があります.
Mathlib の Real は,実装上は有理数の Cauchy 列の同値類として構成されています.
ただし,通常の形式化ではこの構成を直接展開することはほとんどありません.
実用上は,ℝ が体,線形順序,位相空間,完備距離空間,条件付き完備線形順序などの構造を持つ型である,というインターフェースを使います.
プログラミング言語の Float とも違います.
Lean の ℝ は有限精度の浮動小数点数ではなく,証明対象としての数学的な実数です.
丸め誤差を含む計算ではなく,定理として a + b = b + a や中間値の定理,完備性などを扱うための型です.
Mathlib.Data 配下には,実数そのものに加えて,非負実数や拡張実数を表す型が用意されています.
| 型 | notation | 構成 | 主な用途 |
|---|---|---|---|
Real |
ℝ |
有理数 Cauchy 列の同値類 | 通常の実解析,代数,位相 |
NNReal |
ℝ≥0 |
{r : ℝ // 0 ≤ r} |
非負量,距離,ノルム,確率など |
ENNReal |
ℝ≥0∞ |
WithTop ℝ≥0 |
測度,拡張距離,∞ を許す非負量 |
EReal |
なし | WithBot (WithTop ℝ) |
[-∞, ∞] 型の拡張実数,limsup/liminf など |
名前だけ見ると似ていますが,これらは別々の型です.
型が違うものを混ぜるときは,coercion や toReal,ofReal,toNNReal などの変換を確認します.
特に,toReal や ofReal には情報を失うものがあります.
#check Real
#check ℝ
#check NNReal
#check ℝ≥0
#check ENNReal
#check ℝ≥0∞
#check EReal
#synth Field ℝ
#synth LinearOrder ℝ
#synth IsStrictOrderedRing ℝ
#synth Archimedean ℝ
#synth TopologicalSpace ℝ
#synth CompleteSpace ℝ
#synth ConditionallyCompleteLinearOrder ℝ
#synth LinearOrderedCommGroupWithZero ℝ≥0
#synth ConditionallyCompleteLinearOrderBot ℝ≥0
#synth CompleteLinearOrder ℝ≥0∞
#synth CompleteLinearOrder EReal
#check Real.equivCauchy
#check Real.sqrt
#check Real.sqrt_sq_eq_abs
example : ((3 : Nat) : ℝ) + 2 = 5 := by
norm_num
example (x : ℝ) : 0 ≤ |x| := by
exact abs_nonneg x
example (x : ℝ) : Real.sqrt (x ^ 2) = |x| := by
simpa using Real.sqrt_sq_eq_abs x
norm_num は数値計算に強い tactic です.
実数上の具体的な数値等式・不等式を閉じるときによく使います.
Real.sqrt のような標準関数も Real 名前空間に置かれ,補題名も Real.sqrt_sq_eq_abs のように整理されています.
NNReal: 非負実数¶
NNReal は nonnegative real,つまり \([0, \infty)\) に対応する型です.
notation は ℝ≥0 です.
実装は ℝ の subtype で,
です.
要素は実数 r と証明 0 ≤ r の組ですが,通常は coercion により実数としても使えます.
NNReal.mk x hx は,非負性の証明 hx : 0 ≤ x から x : ℝ≥0 を作るコンストラクタです.
一方,Real.toNNReal x は任意の実数を非負実数に送りますが,負の値は 0 に潰します.
したがってこれは単なる型変換ではありません.
#check NNReal.mk
#check NNReal.toReal
#check Real.toNNReal
#check Real.coe_toNNReal
#check Real.toNNReal_of_nonpos
example (x : ℝ≥0) : (0 : ℝ) ≤ (x : ℝ) := by
exact x.property
example (x : ℝ) (hx : 0 ≤ x) : ((Real.toNNReal x : ℝ≥0) : ℝ) = x := by
exact Real.coe_toNNReal x hx
example : ((Real.toNNReal (-3 : ℝ) : ℝ≥0) : ℝ) = 0 := by
norm_num [Real.toNNReal]
ℝ≥0 は非負性を型に持たせたいときに便利です.
たとえば距離,ノルム,確率,非負関数などは,値が常に非負であることを命題として毎回仮定するより,値域を ℝ≥0 にすると扱いやすくなります.
ただし,ℝ≥0 は体ではありません.
負数を持たないので,通常の ℝ と同じ引き算・反数の感覚で使うと詰まります.
ENNReal: 拡張非負実数¶
ENNReal は extended nonnegative real,つまり \([0, \infty]\) に対応する型です.
notation は ℝ≥0∞ で,実装は WithTop ℝ≥0 です.
これは ℝ≥0 に新しい最大元 ∞ を付け加えた型です.
測度論では,測度や非負関数の積分値が ∞ になりうるため,ENNReal がよく現れます.
また,拡張距離 edist の値域としても使われます.
ENNReal には ∞ があるため,toReal は ∞ を 0 に送ります.
また,ENNReal.ofReal は負の実数を 0 に送ります.
したがって,toReal と ofReal は情報を失う変換です.
#check ENNReal.ofReal
#check ENNReal.toReal
#check ENNReal.toNNReal
#check ENNReal.ofReal_ne_top
#check ENNReal.toReal_ofReal
example : (0 : ℝ≥0∞) ≤ ∞ := by
simp
example : (∞ : ℝ≥0∞) + 1 = ∞ := by
simp
example : (1 : ℝ≥0∞) + 2 = 3 := by
norm_num
example : ENNReal.ofReal (-3 : ℝ) = 0 := by
norm_num [ENNReal.ofReal, Real.toNNReal]
example (x : ℝ) (hx : 0 ≤ x) : (ENNReal.ofReal x).toReal = x := by
exact ENNReal.toReal_ofReal hx
example : (∞ : ℝ≥0∞).toReal = 0 := by
rfl
ENNReal.ofReal x は常に有限な ℝ≥0∞ です.
そのため ENNReal.ofReal_ne_top のような補題があります.
一方で,∞ から ℝ に戻すと 0 になるので,toReal を使うときは有限性の仮定 a ≠ ∞ が必要になることが多いです.
EReal: 符号つき拡張実数¶
EReal は extended real,つまり \([-\infty, \infty]\) に対応する型です.
実装は WithBot (WithTop ℝ) で,ℝ に上端 ⊤ と下端 ⊥ を追加したものです.
ENNReal が非負側だけを拡張するのに対し,EReal は正負両方向に無限大を持ちます.
EReal は完備線形順序として扱えるため,順序論的な上限・下限や limsup/liminf のように,正負の無限大が自然に出る場面で使います.
ただし,代数演算には通常の実数とは違う注意点があります.
たとえば EReal.toReal も ⊤ と ⊥ を 0 に送ります.
#check EReal
#check EReal.toReal
#check EReal.toReal_top
#check EReal.toReal_bot
#check EReal.toReal_coe
example : EReal.toReal (⊤ : EReal) = 0 := by
exact EReal.toReal_top
example : EReal.toReal (⊥ : EReal) = 0 := by
exact EReal.toReal_bot
example (x : ℝ) : EReal.toReal (x : EReal) = x := by
exact EReal.toReal_coe x
まとめると,通常の実解析ではまず ℝ を使います.
値が非負であることを型に持たせたいなら ℝ≥0,非負で ∞ も許したいなら ℝ≥0∞,正負両方の無限大を許したいなら EReal を使います.
変換関数の名前を見たら,それが埋め込みなのか,負の値や無限大を 0 に潰す写像なのかを確認するのが重要です.
演習¶
Real,NNReal,ENNReal,EReal の型と変換を確認してください.
#check Real
#check NNReal
#check ENNReal
#check EReal
#synth TopologicalSpace ℝ
#synth ConditionallyCompleteLinearOrder ℝ
#synth CompleteLinearOrder ℝ≥0∞
#synth CompleteLinearOrder EReal
example (x : ℝ) (hx : 0 ≤ x) :
((Real.toNNReal x : ℝ≥0) : ℝ) = x := by
-- 解答例:
-- exact Real.coe_toNNReal x hx
sorry
example : ENNReal.ofReal (-3 : ℝ) = 0 := by
-- 解答例:
-- exact ENNReal.ofReal_of_nonpos (by norm_num)
sorry
不等式の型と証明パターン¶
数学では \(a \le b\) を 1 つの文として読みます.
Lean でも同じく,a ≤ b は命題です.
より正確には,a b : α のとき,a ≤ b は Prop 型の項です.
≤ という notation は LE.le に対応し,< は LT.lt に対応します.
したがって,不等式は実数専用の構文ではありません.
どの型 α の上の順序を使っているかによって,不等式の意味が決まります.
順序の基本性質は型クラスで与えられます.
推移律などを使うには Preorder α,反対称性も使うには PartialOrder α,任意の 2 元が比較できることを使うには LinearOrder α が必要です.
さらに,足し算や掛け算と順序の相性を使うには,IsOrderedAddMonoid,IsOrderedRing,IsStrictOrderedRing などの構造が関わります.
たとえば実数 ℝ には線形順序,体,順序環の構造が入っているので,通常の不等式計算ができます.
#check LE.le
#check LT.lt
#check Preorder
#check PartialOrder
#check LinearOrder
#check IsOrderedAddMonoid
#check IsOrderedRing
#check IsStrictOrderedRing
#check ((3 : ℝ) ≤ 5)
#check ((3 : ℝ) < 5)
#synth LE ℝ
#synth LT ℝ
#synth LinearOrder ℝ
#synth IsOrderedAddMonoid ℝ
#synth IsStrictOrderedRing ℝ
≤ や < は型によって意味が変わります.
たとえば ℝ 上の 0 ≤ x と,ℝ≥0 上の 0 ≤ x は見た目が似ていますが,別々の型の上の順序です.
x : ℝ≥0 は内部的には非負実数なので,実数へ coercion すれば 0 ≤ (x : ℝ) が取り出せます.
#check (fun x : ℝ => 0 ≤ x)
#check (fun x : ℝ≥0 => (0 : ℝ) ≤ (x : ℝ))
#check (fun x : ℝ≥0 => (0 : ℝ≥0) ≤ x)
example (x : ℝ≥0) : (0 : ℝ) ≤ (x : ℝ) := by
exact x.property
また,≤ は数値の大小だけでなく,順序一般を表す記号です.
集合や部分構造では,≤ が包含関係を表すことがあります.
証明中に不等号が現れたら,まず両辺の型を確認するのが安全です.
#check (fun s t : Set ℝ => s ≤ t)
#check (fun s t : Set ℝ => s ⊆ t)
#check (fun S T : AddSubgroup ℤ => S ≤ T)
不等式の基本的な証明は,推移律や変形補題を使って進めます.
数学で「a ≤ b ≤ c だから a ≤ c」と書く部分は,Lean では le_trans や calc で表せます.
example (a b c : ℝ) (h₁ : a ≤ b) (h₂ : b ≤ c) : a ≤ c := by
exact le_trans h₁ h₂
example (a b c : ℝ) (h₁ : a < b) (h₂ : b ≤ c) : a < c := by
exact lt_of_lt_of_le h₁ h₂
example (a b : ℝ) (h : a < b) : ¬ b ≤ a := by
exact not_le_of_gt h
example (a b c d : ℝ) (hab : a ≤ b) (hbc : b ≤ c) (hcd : c ≤ d) : a ≤ d :=
calc
a ≤ b := hab
_ ≤ c := hbc
_ ≤ d := hcd
数値だけの不等式は norm_num が得意です.
仮定を含む一次不等式は linarith が強く,二次式などの多項式不等式は nlinarith が有効なことがあります.
nlinarith は,平方非負性のような補助補題を渡すと使いやすくなります.
example : (3 : ℝ) / 2 < 2 := by
norm_num
example (x y : ℝ) (hx : x ≤ 3) (hy : y ≤ 4) : x + y ≤ 7 := by
linarith
example (x : ℝ) : 0 ≤ x ^ 2 := by
nlinarith [sq_nonneg x]
example (x : ℝ) : 0 < x ^ 2 + 1 := by
nlinarith [sq_nonneg x]
非負性を積み上げる証明では,既存補題を直接使うのが読みやすいことも多いです.
mul_nonneg は「非負数同士の積は非負」,mul_le_mul_of_nonneg_left は「非負数を左から掛けても不等式の向きは変わらない」という補題です.
補題名では,le が ≤,lt が <,nonneg が 0 ≤ ...,pos が 0 < ... を表すことが多いです.
#check add_le_add
#check mul_nonneg
#check mul_le_mul_of_nonneg_left
#check abs_nonneg
#check sq_nonneg
example (a b c d : ℝ) (hab : a ≤ b) (hcd : c ≤ d) : a + c ≤ b + d := by
exact add_le_add hab hcd
example (x y : ℝ) (hx : 0 ≤ x) (hy : 0 ≤ y) : 0 ≤ x * y := by
exact mul_nonneg hx hy
example (x y c : ℝ) (hxy : x ≤ y) (hc : 0 ≤ c) : c * x ≤ c * y := by
exact mul_le_mul_of_nonneg_left hxy hc
単調性に従う変形は gcongr が便利です.
次の例では,両辺に同じ正の項を足す単調性を使っています.
非負性・正値性を自動で示したいときは positivity が役に立つことがあります.
example (x y : ℝ) (hxy : x ≤ y) : x + 1 ≤ y + 1 := by
gcongr
example (x : ℝ) : 0 ≤ x ^ 2 + 1 := by
positivity
実際の証明では,次のように切り分けると方針を立てやすくなります.
- 具体的な数値計算なら
norm_num - 線形不等式なら
linarith - 多項式不等式なら
nlinarithとsq_nonnegなど - 単調性なら
gcongr,またはadd_le_add,mul_le_mul_of_nonneg_leftなど - 長い連鎖なら
calc - 型が混ざるなら,coercion と変換補題を
#checkで確認する
数学者にとって重要なのは,不等式そのものを「文」ではなく「ある型の上の順序関係から作られた命題」と読むことです.
これが分かると,エラーメッセージに出る型クラス仮定や,le,lt,nonneg,pos を含む補題名が読みやすくなります.
演習¶
不等式の型と証明パターンを確認してください.
#check LE.le
#check LT.lt
#synth LinearOrder ℝ
#synth IsStrictOrderedRing ℝ
example (x y : ℝ) (hx : x ≤ 3) (hy : y ≤ 4) : x + y ≤ 7 := by
-- 解答例:
-- linarith
sorry
example (x : ℝ) : 0 ≤ x ^ 2 := by
-- 解答例:
-- positivity
sorry
example (x y c : ℝ) (hxy : x ≤ y) (hc : 0 ≤ c) : c * x ≤ c * y := by
-- 解答例:
-- exact mul_le_mul_of_nonneg_left hxy hc
sorry
ここまでで,よく使う具体的な対象と,それらの上に入っている構造を見ました. 以降は,それらの対象について実際に証明を書くときによく使う補助 tactic と検索の考え方を整理します.
型クラスと名前空間から定理を探す¶
Mathlib のような大きいライブラリでは,最初から正確な定理名を知っていることは多くありません. そのため,次の二つの方向から探すのが実用的です.
- ゴールに出ている型が,どの型クラスインスタンスを持っているかを見る
- 対象の名前空間に,どのような定理や関数があるかを見る
たとえば,+ は型クラス Add の field add から来ています.
型クラスの中身は #print で確認でき,特定の field の型は #check で確認できます.
また,ある型にその型クラスのインスタンスがあるかは #synth で確認できます.
順序や位相構造も同じです.
ℝ が線形順序や位相空間の構造を持つことは,それぞれ #synth で確認できます.
構造そのものを知りたいときは,型クラス名を #print します.
ただし,大きい型クラスは field が多いので,最初は #check と #synth だけでも十分です.
#check LE.le
#check LT.lt
#synth LinearOrder ℝ
#check TopologicalSpace
#check TopologicalSpace.IsOpen
#synth TopologicalSpace ℝ
一方,Continuous.comp のような名前は,型クラスの field ではなく,Continuous という名前空間に置かれた定理です.
hg.comp hf と書けるのは,hg : Continuous g を第 1 引数とする定理 Continuous.comp が dot notation で使われているからです.
このようなものは,「インスタンスが持つメソッド」というより,名前空間に置かれた定理・関数だと読む方が Lean では自然です.
名前を知っているときは #check で型を確認します.
暗黙引数も含めて詳しく見たいときは,名前の前に @ を付けます.
#check Continuous.comp
#check @Continuous.comp
#check Continuous.isOpen_preimage
#check @Continuous.isOpen_preimage
名前空間は,探すときの大きな手がかりです.
集合なら Set.,有限集合なら Finset.,実数なら Real.,準同型なら MonoidHom. というように,対象の型名や構造名に対応する名前空間から候補を探します.
エディタ上では Set. や Real. まで打つと補完候補が出るので,#check と補完を往復するのが実用的です.
#check Set.mem_inter_iff
#check Set.Subset.trans
#check Finset.mem_union
#check Finset.sum
#check Real.sqrt
#check Real.sqrt_sq_eq_abs
#check MonoidHom.map_one
#check MonoidHom.map_mul
まとめると,Mathlib で使えそうな定理を探すときは,次の順に見るとよいです.
- ゴールと仮定に出ている型を
#checkで見る. - その型が持つ構造を
#synthで見る. - 構造や対象の名前空間を補完と
#checkで見る. - 暗黙引数が分からないときは
#check @定理名で詳しく見る. - それでも見つからないときは,後で見る
exact?,rw?,#loogleなどを使う.
Lean には「このインスタンスが持つメソッドを一覧する」という単一の標準コマンドがあるというより,
型クラスの field は #print,インスタンスの存在は #synth,名前空間の定理・関数は補完と #check で調べる,と考えると分かりやすいです.
Mathlib でよく使う証明パターン¶
Mathlib を使う証明では,まず既存の補題を探し,rw,simp,exact,apply,ext などで組み合わせます.
型クラスによって定理が一般化されているため,具体的な型ではなく一般的な構造に対する定理を使うことが多くあります.
ここでは,Core Lean だけで進めた前章から一歩進んで,Mathlib を import した環境でよく使う補助 tactic も扱います.
by_contra,push Not,nth_rw,conv_lhs,aesop,norm_num,linarith,nlinarith,positivity,gcongr,#loogle などは,実用上よく使われますが,Core Lean だけの最小環境では使えないものがあります.
ただし,基本的な見方は前章と同じです.
rw は Eq や Iff の証明項を使った書き換え,ext は外延性補題の適用,norm_num や linarith は数値・不等式の証明項を自動生成する tactic として読むと,Mathlib の証明も追いやすくなります.
example (A B : Set Nat) (x : Nat) : x ∈ A ∩ B ↔ x ∈ A ∧ x ∈ B := by
exact Set.mem_inter_iff x A B
example (A B : Set Nat) (x : Nat) : x ∈ A ∪ B ↔ x ∈ A ∨ x ∈ B := by
exact Set.mem_union x A B
example (a b c : Nat) : a + b + c = a + (b + c) := by
rw [Nat.add_assoc]
example (a b : Nat) : a + b = b + a := by
simpa using Nat.add_comm a b
simpa using ... は,既存の補題の形とゴールが少し違うときに便利です.
補題とゴールの両方を simp で正規化して一致させます.
Mathlib の証明では,既存補題を exact でそのまま使うより,simpa using で少し形を整えて使う場面がよくあります.
norm_num: 数値計算¶
norm_num は,具体的な数値等式・不等式を証明する tactic です.
自然数だけでなく,整数,有理数,実数の数値計算にも使えます.
example : (3 : ℤ) + 4 = 7 := by
norm_num
example : (3 : ℚ) / 2 + 1 / 2 = 2 := by
norm_num
example : (3 : ℝ) ^ 2 + 4 ^ 2 = 25 := by
norm_num
by_contra と push Not¶
by_contra h は背理法の tactic です.
ゴール P を証明するかわりに h : ¬ P を仮定し,ゴールを False に変えます.
Core Lean では Classical.byContradiction を直接使えますが,Mathlib を使う実際の証明では by_contra がよく使われます.
push Not は,否定を量化子や論理結合子の内側へ押し込む tactic です.
たとえば古典論理のもとで,¬ ∀ x, P x は ∃ x, ¬ P x に変形されます.
演習¶
by_contra と push Not を使って,古典論理の証明を書いてください.
example (P : Prop) (h : ¬¬ P) : P := by
-- 解答例:
-- by_contra hP
-- exact h hP
sorry
example (P : Nat → Prop) (h : ¬ ∀ n, P n) : ∃ n, ¬ P n := by
-- 解答例:
-- push Not at h
-- exact h
sorry
nth_rw と conv_lhs¶
rw は通常,該当する箇所をまとめて書き換えます.
一部だけを書き換えたいときには,nth_rw や conv モードを使います.
nth_rw 1 [h] は,1 番目に現れる該当箇所だけを書き換えます.
conv_lhs は,等式の左辺に入って書き換えるための短い notation です.
Core Lean の conv => lhs と同じ発想ですが,短く書けるため実用上よく使われます.
演習¶
nth_rw または conv_lhs を使って,ゴールの一部だけを書き換えてください.
example (a b c : Nat) (h : a + b = c) :
(a + b) + (a + b) = c + (a + b) := by
-- 解答例:
-- nth_rw 1 [h]
sorry
example (a b c : Nat) : (a + b) + c = (b + a) + c := by
-- 解答例:
-- conv_lhs =>
-- rw [Nat.add_comm a b]
sorry
aesop¶
aesop は,論理規則や登録された補題を使って探索する自動証明 tactic です.
命題論理,単純な述語論理,コンストラクタによる証明に強く,短い補助目標を閉じるときに便利です.
example (P Q R : Prop) (hPQ : P → Q) (hQR : Q → R) (hP : P) : R := by
aesop
example (P Q : Prop) (h : P ∧ Q) : Q ∧ P := by
aesop
演習¶
aesop で閉じる論理問題を作り,手動証明と比較してください.
検索支援: exact?,rw?,#loogle¶
exact?,rw?,try? は,現在のゴールを閉じる候補や書き換え候補を提案します.
出力は Lean や Mathlib のバージョンによって変わることがあるため,ここでは実行例として示します.
example (n : Nat) : n + 0 = n := by
exact?
example (a b : Nat) : a + b = b + a := by
rw?
example (P Q : Prop) (h : P ∧ Q) : Q ∧ P := by
try?
#loogle は,式の形やキーワードから Mathlib の定理を探すためのコマンドです.
環境によっては Loogle サーバへの接続が必要です.
Mathlib の命名規則¶
Mathlib の名前には強い規則性があります. 命名規則を知っていると,補題を推測したり,検索結果を読むのが楽になります.
基本方針は次の通りです.
- 定理名や証明項は
snake_case:add_comm,mul_assoc,not_le_of_gt - 型,命題,
structure,クラスはUpperCamelCase:Finset,MonoidHom,LinearOrder - 通常のデータや関数は
lowerCamelCase - 名前空間はドットで表す:
Nat.succ_ne_zero,Set.mem_inter_iff - 写像が構造を保つ補題は
map_add,map_mul,map_zeroのような名前になりやすい
ただし,古い名前や歴史的事情による例外もあります. 命名規則は検索の手がかりであって,完全な規格ではありません.
#check Nat.succ_ne_zero
#check Set.mem_inter_iff
#check Set.mem_union
#check add_comm
#check mul_assoc
#check map_add
記号に対応する語もある程度決まっています.
たとえば,∈ は mem,∩ は inter,∪ は union,≤ は le,< は lt,↔ は iff です.
したがって,集合の所属条件を探すときには mem_inter や mem_union のような名前を予想できます.
また,定理名では結論が先に来ることがよくあります.
たとえば not_le_of_gt は「> から ¬ ≤ が従う」と読みます.
この名前は,結論 not_le と仮定 of_gt に分けて読むと分かりやすくなります.
命名規則は公式の Mathlib naming conventions に整理されています. 新しい補題名を探すときは,記号を英語名に直し,名前空間と結論の形から推測するのが有効です.
演習¶
命名規則を使って,次の補題名を予想し,#check で確認してください.
まとめ¶
Mathlib を使う証明では,具体的な対象だけでなく,それが持つ型クラスインスタンスを意識することが重要です.
Set α は型 α 上の部分集合全体を表し,Finset α は有限集合をデータとして持つ型です.
また,ℝ,ℝ≥0,ℝ≥0∞,EReal は別々の型であり,用途に応じて選びます.
それぞれに登録された型クラスインスタンスと,型の間の coercion・変換関数を確認することが重要です.
不等式 a ≤ b や a < b は,両辺の型に入っている順序構造から作られる命題です.
数値計算,線形不等式,多項式不等式,単調性,型変換を切り分けると,使う tactic や補題を選びやすくなります.
Mathlib の証明は既存補題の組み合わせです.
#check,#synth,命名規則,simp,rw,ext,norm_num,linarith,nlinarith を使いながら,ゴールに合う補題を探して適用します.
大きな流れとしては,まず型クラスや bundled structure などの一般的な読み方を押さえ,その上で Set,Finset,実数,不等式の各対象に入ると,Mathlib の定理の形が見通しやすくなります.
Chapter 05: Lean Project の作成と操作¶
この章では,Lean project を新しく作る方法,既存 project を開く方法,Lean のバージョンを指定する方法, Mathlib を依存関係として追加する方法,Lake の基本操作を説明します.
Lean のファイルは,単独の .lean ファイルとして扱うより,基本的には project の中で扱います.
project は,lean-toolchain,lakefile.toml または lakefile.lean,lake-manifest.json,
ソースディレクトリ,.lake/ 以下の依存関係とビルド成果物をまとめて管理する単位です.
参考:
- Lean Community, Lean projects: https://leanprover-community.github.io/install/project.html
- Lean のインストール方法・elan と Lake の使い方,Lean プロジェクトの作り方: https://aconite-ac.github.io/how_to_install_lean/lake-package-manager/how-to-create-project.html
Mathlib を含む Lean project は容量をかなり使います.
Mathlib の依存パッケージ,ビルド済みキャッシュ,.lake/ 以下の成果物を含めると,少なくとも 7.2 GB 程度の空き容量を見込んでください(2026年6月時点の実績).
空き容量が少ない状態で lake update や lake exe cache get を実行すると,途中で失敗したり,壊れたキャッシュが残ったりします.
Elan, Lean, Lake¶
Lean project を扱うときに出てくる主な道具は次の 3 つです.
| 名前 | 役割 |
|---|---|
elan |
Lean toolchain manager.どの Lean バージョンを使うかを選ぶ. |
lean |
Lean 本体..lean ファイルを読み,型検査や elaboration を行う. |
lake |
Lean の package manager / build tool.project 作成,依存関係,ビルドを管理する. |
通常,ユーザーが直接呼び出す lean や lake は Elan の proxy です.
カレントディレクトリか親ディレクトリに lean-toolchain があれば,Elan はそこに書かれた toolchain を使います.
たとえばこの project の lean-toolchain は次のような 1 行のファイルです.
この指定により,この project では Lean 4.30.0 系の lean と lake が使われます.
Lean バージョンの指定法¶
Lean のバージョン指定は,project ルートの lean-toolchain に書くのが基本です.
このファイルを commit しておくと,別の計算機で project を開いたときにも同じ Lean バージョンが使われます. 講義資料,論文付録,研究プロジェクトでは,Lean と Mathlib のバージョンを固定することが再現性のために重要です.
一時的に特定バージョンのコマンドを使いたい場合は,Elan の +toolchain 記法を使えます.
ただし,project の通常運用では lean-toolchain を編集して管理する方が分かりやすいです.
lake +v4.30.0 new my_project math のように書いた場合,そのコマンドを実行する lake のバージョンを指定しているのであって,
作られる project の Lean バージョンを恒久的に指定する主な方法は,作成後の lean-toolchain です.
現在使われている toolchain を確認するには次を使います.
Mathlib を含まない project を作る¶
Mathlib を使わない最小 project は次で作れます.
lake new my_project は,現在のディレクトリの下に my_project/ を作り,その中に Lake package を生成します.
生成される典型的な構成は次のようなものです.
Lean の module 名はファイルパスに対応します.
たとえば MyProject/Basic.lean は,通常 import MyProject.Basic で import できます.
lake init my_project という作り方もあります.
これは,すでに存在する空ディレクトリの中で project を初期化したいときに使います.
Mathlib を含む project を作る¶
数学の形式化では,ほとんどの場合 Mathlib を使います. Mathlib 付きの project は次で作ります.
math は,Mathlib を依存関係に含める template です.
これにより,project 内の Lean ファイルで
または
のように書けるようになります.
lake update は依存関係を解決し,lake-manifest.json を更新します.
lake exe cache get は Mathlib のビルド済みキャッシュを取得します.
Mathlib 全体を手元で最初からビルドすると時間がかかるため,通常はキャッシュを取得してから作業します.
注意: Mathlib を含む project では,.lake/ と Mathlib cache に大きな容量が必要です.
少なくとも 7.2 GB 程度の空き容量を確保してから作成してください.
既存 project に Mathlib を追加する¶
すでにある Lean project に Mathlib を追加するには,Lean バージョンと Mathlib のバージョンを揃える必要があります.
Mathlib は Lean 本体の特定バージョンに依存しているため,適当な Lean バージョンと適当な Mathlib revision を混ぜると,
lake update や import Mathlib でエラーになります.
この資料のように lakefile.toml を使う場合,たとえば Lean 4.30.0 に対応する Mathlib release を使うなら,
lean-toolchain を次のようにします.
そして lakefile.toml に Mathlib の依存関係を追加します.
追加後,project ルートで次を実行します.
lakefile.lean を使う project なら,依存関係は次のように書きます.
Mathlib の最新版 master に追随する場合は,Mathlib 側の lean-toolchain に Lean バージョンを合わせます.
curl -L https://raw.githubusercontent.com/leanprover-community/mathlib4/master/lean-toolchain -o lean-toolchain
lake update
lake exe cache get
ただし,講義資料や共同研究では,毎回 master に追随するより,特定の release tag や commit hash に固定する方が安定です.
既存 project を開く¶
GitHub などから既存 project を取得したら,まず project ルートに移動します.
Mathlib を使う project なら,通常は次を実行します.
lake update は lake-manifest.json を更新することがあるため,
単に既存 project を使うだけなら,まず lake exe cache get と lake build で足りることもあります.
共同開発中の project で lake-manifest.json を勝手に更新したくない場合は,実行前に差分が出てもよいか確認してください.
VS Code で開くときは,LeanMathNote/ のようなソースディレクトリではなく,lakefile.toml と lean-toolchain がある project ルートを開きます.
Lake の基本操作¶
よく使う Lake コマンドをまとめます.
lake build # project 全体をビルドする
lake build MyProject.Basic # 特定 module / target をビルドする
lake env lean Foo.lean # project の依存関係を含む環境で Lean を起動する
lake update # 依存関係を解決し lake-manifest.json を更新する
lake exe cache get # Mathlib のビルド済みキャッシュを取得する
lake clean # この package の build outputs を削除する
lake clean は,ビルド成果物を消して作り直したいときに使います.
典型的には .lake/build/ 以下の成果物が対象であり,Lean toolchain そのものや,
Mathlib の全キャッシュを完全に削除するためのコマンドではありません.
依存関係や cache まで含めて大きく掃除したい場合は,何を消すかを理解してから手動で行ってください.
容量を確認したいときは,たとえば次のようにします.
macOS や Linux では,.lake/ が project 内で大きくなりやすいディレクトリです.
不要になった実験 project は,project ディレクトリごと削除して構いません.
一方で,作業中の project の .lake/ を消すと,依存関係やビルド成果物を再取得・再ビルドする必要があります.
project の中で Lean ファイルを確認する¶
project の依存関係を使って 1 ファイルを確認したいときは,lake env lean を使います.
import Mathlib を含むファイルを素の lean MyProject/Basic.lean で実行すると,
Mathlib が見つからないことがあります.
lake env は,Lake workspace の依存関係を含む環境変数を設定したうえでコマンドを実行します.
この project なら,たとえば次のように章単体を確認できます.
前者はファイルパスで Lean を直接実行し,後者は Lake target として module をビルドします.
よくある失敗¶
Lean project の操作でよくある失敗をまとめます.
lakefile.tomlがある directory ではなく,その下のソース directory を VS Code で開いている.lean-toolchainと Mathlib の revision が対応していない.lake update後にlake exe cache getを実行しておらず,Mathlib を手元で長時間ビルドしている.- 空き容量が足りない.Mathlib project では 7.2 GB 程度の余裕を見込む.
lake updateによってlake-manifest.jsonが変わったことに気づかず commit してしまう.lake cleanを cache 削除コマンドだと思っている.これは主に build outputs を消すためのコマンドである.
困ったときは,まず次を確認します.
まとめ¶
Lean project は,Lean ファイルの置き場というだけでなく,Lean バージョン,依存関係,module,ビルド成果物をまとめて管理する単位です.
- Lean バージョンは
lean-toolchainで固定する. - Mathlib なしなら
lake new my_projectでよい. - Mathlib ありなら
lake new my_project mathの後にlake updateとlake exe cache getを実行する. - 既存 project に Mathlib を追加するときは,
lean-toolchainと Mathlib revision を対応させる. lake build,lake env lean,lake cleanの役割を区別する.
この章の内容を押さえると,次章の Lean の内部構造や build artifact の説明を読みやすくなります.
演習問題¶
-
空き容量を確認してください.Mathlib を含む project を作る前に,7.2 GB 程度の余裕があるか確認します.
-
Mathlib を含まない project を一時ディレクトリに作り,
lake buildしてください. -
Mathlib を含む project を作る手順を,実行せずに説明してください.
-
lean-toolchainを開き,どの Lean バージョンが指定されているか確認してください. -
この project の
lakefile.tomlを読み,[[lean_lib]]と[[require]]が何を指定しているか説明してください. -
lake cleanを実行すると何が消えるか,実行前に説明してください. 講義中に実行する場合は,git status --shortとdu -sh .lakeを先に確認してください.
Chapter 06: Lean の仕組み¶
この章では,Lean を「証明を書くための言語」としてだけでなく, 「ソースコードを読み,エラボレートし,カーネルで検査し,ビルド成果物を作るシステム」として眺めます.
扱う内容は次の通りです.
- Lean の処理パイプライン
- カーネル,エラボレータ,抽象構文木
.oleanと.ilean- VS Code を使わずに Lean と相互作用する方法
- プロジェクトのファイル構造と module
参考:
- Lean Language Reference: https://lean-lang.org/doc/reference/latest/
- Elaboration and Compilation: https://lean-lang.org/doc/reference/latest/Elaboration-and-Compilation/
- Interacting with Lean: https://lean-lang.org/doc/reference/latest/Interacting-with-Lean/
- Source Files and Modules: https://lean-lang.org/doc/reference/latest/Source-Files-and-Modules/
- Build Tools and Distribution: https://lean-lang.org/doc/reference/latest/Build-Tools-and-Distribution/
注意として,Lean Language Reference の latest は常に最新系列の Lean を説明しています.
この原稿では,Lean project の作成や Lean version の指定方法は Chapter 05 にまとめ,
この章では Lean がソースコードをどのように処理するかに焦点を当てます.
細かいコマンド出力やビルド成果物の名前は Lean のバージョンによって少し異なることがあります.
この章では,概念は Reference に沿って説明し,実行例はこのプロジェクトの Lean 4.30.0 で確認できる形にしています.
Lean の処理パイプライン¶
Lean が .lean ファイルを読むときの流れは,大まかには次のようになります.
これは概念図であり,実装が常にこの順にファイル全体を一括処理するという意味ではありません.
文字列としてのソースコード
↓ parsing
抽象構文木 Syntax
↓ macro expansion
展開後の Syntax
↓ elaboration
コア言語の式 Expr
↓ kernel checking
カーネルに受理された定義・定理
↓ serialization / compilation
.olean, .ilean, C code, native object など
実際には,ファイル全体を一度に処理するというより,トップレベルの command ごとに 「構文解析,マクロ展開,エラボレーション,カーネル検査」が進みます. ある command によって新しい記法や定義が追加されると,次の command はその更新後の環境で解釈されます.
また,マクロ展開はエラボレーションから完全に独立した前処理ではありません. Reference の言い方では,macro expansion は elaboration の一部であり, 外側の構文を展開してからその層をエラボレートし,内側に残ったマクロはエラボレータがそこに到達したときに展開されます. したがって,上の図の「macro expansion -> elaboration」は,学習用に分けて見せた模式的な区分です.
この図の各段階で現れるものは,おおよそ次のように考えるとよいです.
| 段階 | 実体 | 見方 |
|---|---|---|
| 文字列としてのソースコード | .lean ファイルや標準入力から渡される Unicode 文字列です.まだ Lean の構文木ではありません. |
エディタで開く,cat LeanMathNote/basic/chapter06.lean で見る,lake env lean --stdin に文字列を渡す. |
抽象構文木 Syntax |
parser が作る Lean.Syntax 型の値です.記号列の木構造とソース位置は持ちますが,型や意味はまだ決まっていません. |
Lean.Parser.runParserCategory を使うと,小さな文字列を Syntax にできます. |
展開後の Syntax |
macro expansion によって,表面構文をより基本的な構文へ置き換えた後の Syntax です.型づけ済みの式ではなく,まだ構文です. |
syntax,macro,macro_rules で自分でマクロを定義できます.全ファイルの展開後構文を普段の開発で直接見ることはあまりありません. |
コア言語の式 Expr |
term elaborator が作る Lean.Expr 型の値です.暗黙引数,型クラス,overload,tactic などが解決された,カーネルが検査する対象に近い式です. |
#check,#reduce,#print,set_option pp.all true で pretty printer 越しに見ることができます.メタプログラムでは Lean.Elab.Term.elabTerm が Expr を返します. |
| カーネルに受理された定義・定理 | Environment に登録された定数情報です.名前,型,定義本体や定理の証明項などが ConstantInfo として保存されます. |
#check name,#print name,#print axioms name,メタプログラムでは Environment.find? で調べます. |
処理を行う実体も分けておくと,どこをユーザーが拡張できるかが見えやすくなります.
| 処理 | 行う実体 | ユーザーが触れるもの |
|---|---|---|
| parsing | Lean の parser です.parser table は import 済みの構文拡張や開いている namespace の影響を受けます. | syntax,notation で新しい構文を追加できます.小さな例なら Parser.runParserCategory で parser を直接呼べます. |
| macro expansion | elaborator から呼ばれる macro expander です.syntax kind から macro 実装を探し,Syntax を別の Syntax に変換します. |
macro,macro_rules,notation で拡張できます.macro の実装は概念的には Syntax -> MacroM Syntax です. |
| elaboration | command elaborator,term elaborator,tactic elaborator です.構文を解釈し,環境を更新し,Expr や証明項を作ります. |
普通の def,theorem,tactic はここを使います.高度な用途では elab,elab_rules,自作 tactic で拡張できます. |
| kernel checking | Lean の信頼されるカーネルです.エラボレータが作った定義や証明項がコア型理論の規則に従うか検査します. | ユーザーがカーネル自体を Lean コードから拡張することは通常ありません.def,theorem,inductive を書くと,追加前に検査されます. |
| serialization | 検査済みの環境や対話用情報をファイルへ保存する処理です.module を再利用するための .olean などが作られます. |
lake build や lean の実行で発生します.ユーザーは通常,生成物を直接編集せず,import や leanchecker を通して利用します. |
Syntax は parser が作る構文木です.
Expr はエラボレータが作る,Lean のコア型理論に近い式です.
Environment は,これまでに宣言された定義,定理,記法,型クラスインスタンス,属性などを保持する環境です.
実際の対話環境では,これに加えて proof state,識別子の位置,補完候補などの情報も記録され,
VS Code などのフロントエンドがそれを利用します.
ここでいう「コア」は,Lean Language Reference の core type theory や core language に対応する語です.
ユーザーが書く Lean コードそのものを指すのではなく,エラボレータが作り,カーネルが検査するための小さな型理論を指します.
したがって,def,theorem,match,tactic script,型クラス探索,暗黙引数の補完,記法やマクロなどは,そのままコア型理論の構成要素ではありません.
これらはユーザー向けの表面構文やエラボレーションの仕組みであり,最終的には Expr として表されるコア言語の項へ変換されます.
また,実行ファイルを作るための compiler intermediate representation や C code,native object も,コア型理論そのものではなく,コンパイルのための別の表現です.
この区別により,Lean は便利な表面構文を持ちながら,信頼するカーネルを小さく保てます.
抽象構文木¶
Lean の parser は,文字列を Syntax 型の木に変換します.
これはまだ「意味づけ」されていません.
たとえば x + y という構文だけを見ても,自然数の足し算か,群の演算か,行列の足し算かは分かりません.
この段階では,記号の並びとその木構造が分かっているだけです.
Lean では parser や syntax がユーザー拡張可能です.
notation,syntax,macro によって新しい表面構文を追加できます.
各 syntax node には kind があり,エラボレータやマクロ展開器はこの kind を手がかりに処理を選びます. また,parser から直接作られた syntax には元のソース位置や空白に関する情報も残るため, エラー表示,hover,ジャンプ,pretty printing などの基礎にもなります.
#check Lean.Syntax.getKind
#check Lean.Parser.runParserCategory
#eval show CoreM Unit from do
let result := Lean.Parser.runParserCategory (← getEnv) `term "x + y"
match result with
| .ok stx =>
IO.println s!"kind = {Syntax.getKind stx}"
| .error e =>
IO.println e
Syntax.getKind は syntax node の種類を取り出す関数です.
Parser.runParserCategory は,指定した parser category で文字列を解析するための関数です.
通常の証明では直接使いませんが,Lean のフロントエンドが「文字列から syntax tree を作る」ことを確認できます.
上の例では,term category として "x + y" を解析し,得られた syntax node の kind を表示しています.
木全体を見たい場合は,IO.println (repr stx) のように repr を出力すると,Syntax.node,Syntax.ident,Syntax.atom などの構造が見えます.
マクロ展開とエラボレータ¶
エラボレーションとは,ユーザー向けの構文を,Lean のコア型理論の式へ変換する処理です. この処理は単純な翻訳ではありません. 次のような作業も行います.
- 省略された暗黙引数を補う.
- 型を推論する.
- 型クラスインスタンスを探索する.
- overloaded notation の意味を決める.
- tactic script を証明項へ変換する.
- 再帰定義や pattern matching をコア言語が扱える形に変換する.
そのため,エラボレータは Lean の使いやすさの大部分を担っています. 一方で,最終的にできた項はカーネルによって検査されます.
エラボレーションには大きく分けて command elaboration と term elaboration があります.
command elaboration は,def,theorem,#check,open などのトップレベル command を処理し,
必要に応じて環境を更新します.
term elaboration は,型注釈,定義の右辺,定理の証明項などを,期待される型の情報も使いながら Expr にします.
tactic の実行は term elaboration の特殊な場合と考えられ,最終的には証明項を構成します.
syntax "foo_demo_true" : term
macro_rules
| `(foo_demo_true) => `(True)
#check foo_demo_true
#check Lean.Macro
#check Lean.Elab.expandMacroImpl?
#check Lean.Elab.Command.CommandElabM
#check Lean.Elab.Command.elabCommandTopLevel
#check Lean.Elab.Term.TermElabM
#check Lean.Elab.Term.elabTerm
#check Lean.Elab.Tactic.TacticM
#check Lean.Meta.MetaM
foo_demo_true は,この章のために定義した小さな term macro です.
parser はまず foo_demo_true を Syntax として読みます.
その後,macro expansion によって True という構文に置き換えられ,term elaborator がそれを Prop の式として解釈します.
このように,macro は型のついた式を直接返すのではなく,構文を構文へ変換します.
CommandElabM,TermElabM,TacticM は,それぞれ command,term,tactic の elaborator が動くモナドです.
通常の証明を書く段階では意識しませんが,Lean で新しい command や tactic を作るときには,これらの世界でプログラムを書きます.
def fooDouble (n : Nat) : Nat :=
n + n
example : fooDouble 3 = 6 := by
rfl
#check fooDouble
#reduce fooDouble 3
#eval fooDouble 3
#check は型を表示します.
#reduce は,定義の展開や再帰子の計算規則など,Lean の definitional equality に関係する簡約で式を正規化します.
#eval はコンパイル・実行によって値を表示します.
証明では #check で名前の型を調べ,#reduce で定義の計算内容を確認し,#eval で実行可能なプログラムを試す,という使い分けをします.
#reduce と #eval はどちらも「計算結果」を見るコマンドですが,前者は証明で使われる簡約の見え方を確認するもの,
後者は実行可能なコードを実際に走らせるもの,と区別しておくとよいです.
set_option pp.all true を使うと,pretty printer が通常は隠している情報も多く表示します.
暗黙引数や universe,型クラス引数が見えるため,Lean がどれだけ多くの情報を補っているかを確認できます.
カーネル¶
Lean の信頼の中心はカーネルです. カーネルは,エラボレータが作った定義や証明項が,Lean のコア型理論の規則に従っているかを検査します.
重要なのは,tactic やエラボレータは大きく複雑であっても,最終的な証明項はカーネルで検査されるという点です. したがって,通常の tactic にバグがあっても,間違った証明項を作ればカーネルで拒否されます.
逆に言うと,カーネルは Lean のすべての機能を直接知っているわけではありません.
Reference では,カーネルはコア型理論の type checker であり,構文上の termination checker や unification は含まない,と説明されています.
再帰定義や pattern matching は,エラボレータ側で再帰子,整礎再帰,partial fixpoint などを使う形へ変換されます.
通常の数学的証明で使う定理は最終的にこの検査を通りますが,partial や unsafe を含むプログラム実行の話は,
論理的な証明項の検査とは分けて理解する必要があります.
ただし,カーネルが検査するのは「形式化された命題が証明されたか」です. その命題が人間の意図した数学的主張を正しく表しているか,どの公理や imported theorem に依存しているかは別の確認事項です.
#check Eq.refl
#check Nat.rec
#check False
theorem fooKernelExample (n : Nat) : n = n :=
Eq.refl n
#print fooDouble
#print fooKernelExample
#print axioms fooKernelExample
#check Lean.Declaration
#check Lean.ConstantInfo
#check Lean.ConstantInfo.defnInfo
#check Lean.ConstantInfo.thmInfo
#check Lean.DefinitionVal
#check Lean.TheoremVal
#check Lean.Environment.find?
#check Lean.addDecl
#check Lean.addAndCompile
Eq.refl n は等式の反射律を表す証明項です.
by rfl のような tactic は,このような証明項を作るための便利な表面構文だと考えられます.
#print fooDouble や #print fooKernelExample は,環境に登録された名前の型や本体を pretty printer で表示します.
これは .lean ファイルに書いた文字列そのものではなく,エラボレーションとカーネル検査を通って環境に入った後の定義・定理を表示している,と考えるとよいです.
#print axioms fooKernelExample は,その定理がどの公理に依存しているかを調べるコマンドです.
内部的には,環境に追加される宣言は Declaration や ConstantInfo として表されます.
たとえば定義は ConstantInfo.defnInfo,定理は ConstantInfo.thmInfo のような形で区別されます.
addDecl や addAndCompile は,メタプログラム側から新しい宣言を追加するための関数であり,通常の def や theorem の裏側で起こる処理の雰囲気を知る手がかりになります.
.olean と .ilean¶
モジュールをビルドすると,Lean は処理済みの環境情報を .olean ファイルに保存します.
import されたモジュールを毎回ソースからエラボレートし直すのではなく,.olean を読み込むことで高速に再利用します.
この保存が,上のパイプラインでいう serialization です.
保存されるのはソースコードのコピーではなく,カーネル検査済みの宣言,module の環境情報,import に必要な情報などです.
このプロジェクトの Lean 4.30.0 では,言語サーバー用の索引として .ilean も生成されます.
これは補完,ジャンプ,情報表示などのためのデータです.
一方,Reference 最新版の module system の説明では,環境は public,private,server 情報に分けて .olean として保存される,と説明されています.
つまり,正確なファイル名や分割方法は Lean のバージョンや module system の利用状況に依存します.
授業では「.olean は import される検査済み環境,language server 用の情報は対話機能のための補助データ」と押さえれば十分です.
.ilean などの中身は実装詳細なので,通常の開発では直接読む対象ではありません.
このプロジェクトで lake build を実行すると,典型的には次のようなファイルができます.
.lake/build/lib/lean/LeanMathNote.olean
.lake/build/lib/lean/LeanMathNote.ilean
.lake/build/lib/lean/LeanMathNote/basic/chapter06.olean
.lake/build/lib/lean/LeanMathNote/basic/chapter06.ilean
.lake/build/lib/lean/LeanMathNote/practice/chapter01.olean
.lake/build/lib/lean/LeanMathNote/practice/chapter01.ilean
この章のように LeanMathNote/basic/chapter06.lean を単体で lake env lean した場合は,
チェックは行われますが,Lake の通常ビルド対象に入っていなければ .olean は残らないことがあります.
VS Code を使わずに Lean と相互作用する¶
VS Code 拡張は Lean language server の使いやすいフロントエンドです. しかし,Lean 自体はコマンドラインからも使えます.
このプロジェクトでは,まず lake env を付けるのが安全です.
これにより,Mathlib など,Lake workspace の依存関係を含んだ環境で lean が起動します.
lake env lean LeanMathNote/basic/chapter06.lean
lake lean LeanMathNote/basic/chapter06.lean
lake build
lean に直接入力を渡すこともできます.
エラーや情報を機械可読にしたい場合は --json が使えます.
language server をエディタなしで起動するには次を使います.
lean --server は LSP クライアントと通信するためのモードです.
人間が直接対話する REPL というより,エディタや外部ツールが Lean と通信するための入口です.
便利な調査コマンドもあります.
lake env lean --deps LeanMathNote/basic/chapter06.lean
lake env lean --src-deps LeanMathNote/basic/chapter06.lean
lake env lean --profile LeanMathNote/basic/chapter06.lean
lean --print-prefix
lean --print-libdir
lake query LeanMathNote
--deps は import されるモジュールの依存関係を調べます.
--profile は定義や定理ごとの処理時間を見ます.
--print-prefix と --print-libdir は,現在の Lean toolchain がどこにあるかを確認します.
プロジェクトのファイル構造¶
Lean プロジェクトは,通常 Lake workspace として管理されます. このプロジェクトでは,主なファイルは次のような役割を持ちます.
lean-toolchain 使用する Lean toolchain の指定
lakefile.toml package,依存関係,ライブラリ target の設定
lake-manifest.json 依存パッケージの具体的な revision の固定
LeanMathNote.lean ライブラリのルート module
LeanMathNote/ Lean ソースファイル
.lake/ Lake が管理する依存関係とビルド成果物
lake-manifest.json は依存関係のバージョンを固定するため,通常はソース管理に含めます.
一方,.lake/ はビルド成果物やダウンロード済み依存関係を含む作業ディレクトリなので,普通はソース管理に含めません.
これらのファイルをどのように作るか,Mathlib をどう追加するか,lake clean などの操作をどう使うかは Chapter 05 で扱います.
ここでは,module を import するときに .lean ファイルが .olean として再利用される,という処理の流れだけを意識してください.
module と import¶
Lean のファイルは import 可能な単位として扱われます. import で使う module 名は,ソースルートからのパスに対応します.
たとえば次のように対応します.
LeanMathNote.lean module LeanMathNote
LeanMathNote/basic/chapter01.lean module LeanMathNote.basic.chapter01
LeanMathNote/basic/chapter06.lean module LeanMathNote.basic.chapter06
LeanMathNote/practice/chapter01.lean module LeanMathNote.practice.chapter01
別の module を使うには,ファイル冒頭で import します.
import Mathlib は Mathlib 全体を読み込むので講義資料では便利です.
実際の大きな開発では,ビルド時間を抑えるために必要な module だけを import します.
Lean Reference 最新版では,module header を使った module system のもとで,
public,private,import all,@[expose] などによって公開範囲や定義本体を展開できる範囲を制御する話も出てきます.
これは,ほかの module から名前を参照できるか,定義本体を unfolding できるか,変更時にどこまで再ビルドが必要になるかに関係します.
通常の Mathlib 利用やこの講義資料では,まず「ファイルパスと import 名の対応」と namespace の違いを理解すれば十分です.
namespace ModuleExample
def localDefinition : Nat :=
10
example : localDefinition = 10 := by
rfl
end ModuleExample
namespace は module とは別の仕組みです.
module はファイル単位の読み込み単位で,namespace は名前の階層を作る仕組みです.
同じ module の中に複数の namespace を置くことも,複数の module に同じ namespace の定義を分散させることもできます.
leanchecker¶
通常の開発では,lean や lake build によって各宣言がカーネルで検査されます.
さらに .olean に保存された環境を再検査するための道具として leanchecker があります.
概念的には,次のように使います.
通常の授業や演習ではここまで行う必要はありません.
ただし,「Lean の信頼は最終的に小さなカーネル検査に帰着する」という話をするときに,
.olean と leanchecker は重要なキーワードです.
まとめ¶
Lean の仕組みを理解するうえで,次の対応を押さえておくと見通しがよくなります.
- parser は文字列を
Syntaxにする. - macro expansion は表面構文をより基本的な構文へ変換する.実際には elaboration と相互に入り組んで進む.
- elaborator は
Syntaxを型つきのExprにし,暗黙引数,型クラス,tactic,再帰定義などを処理する. - kernel は
Exprがコア型理論の規則に従うかを検査する. .oleanは検査済み環境を保存し,import を高速化する.- このプロジェクトの Lean 4.30.0 では
.ileanが language server のための索引として生成される.
普段の証明では,これらをすべて意識する必要はありません. しかし,エラーの原因を切り分けるとき,依存関係を管理するとき,AI や外部ツールから Lean を呼び出すときには,この構造を知っていることが役に立ちます.
演習問題¶
この章の演習では,Lean の内部構造と,VS Code を介さない Lean との相互作用を確認します. 証明を作るだけでなく,実際にターミナルで出力を読むことを目標にしてください.
-
VS Code を使わずに,この章を単体でチェックしてください.
-
標準入力から Lean にコードを渡してください.
-
--jsonを付けて Lean を実行し,通常の出力との違いを確認してください. -
--depsと--src-depsを使って,依存関係の出力を比較してください. -
Syntax,Expr,Environmentの型を確認してください. -
pretty printer の出力を詳しくして,暗黙引数や型クラス引数が表示されることを確認してください.
-
lake buildを実行した後,生成される.oleanと.ileanを探してください. -
module 名とファイルパスの対応を説明してください.
Chapter 01: 代数¶
Mathlib における代数構造の扱いを概観します.
参考:
- Mathematics in Lean, 9. Groups and Rings: https://leanprover-community.github.io/mathematics_in_lean/C09_Groups_and_Rings.html
中心になる考え方は次の 3 つです.
- 代数構造は型クラスで表す.
- 準同型や部分構造は bundled structure として表す.
- 具体的な計算には
simp,group,abel,ringなどの tactic を使う.
代数構造は型クラスで表す¶
Monoid M,Group G,Ring R,Field K などは型 M,G,R,K に入っている構造を表す型クラスです.
数学では「群 G」と言うことが多いですが,Lean では「型 G と,その上の群構造 [Group G]」を分けて書きます.
#check Monoid
#check CommMonoid
#check Group
#check AddCommGroup
#check Semiring
#check Ring
#check CommRing
#check Field
section BasicStructures
variable {M : Type*} [Monoid M]
example (x : M) : x * 1 = x := by
simp
example (x y z : M) : (x * y) * z = x * (y * z) := by
exact mul_assoc x y z
variable {A : Type*} [AddCommMonoid A]
example (x y : A) : x + y = y + x := by
exact add_comm x y
end BasicStructures
Monoid は乗法的な記法を使います.
加法的に書きたい場合は AddMonoid や AddCommGroup を使います.
同じ数学的事実でも,乗法的な構造では mul_assoc,加法的な構造では add_assoc のように名前が分かれます.
section Groups
variable {G H : Type*} [Group G] [Group H]
example (x : G) : x * x⁻¹ = 1 := by
simp
example (x y z : G) : x * (y * z) * (x * z)⁻¹ * (x * y * x⁻¹)⁻¹ = 1 := by
group
example (f : G →* H) (x : G) : f (x⁻¹) = (f x)⁻¹ := by
exact map_inv f x
end Groups
section AdditiveGroups
variable {A : Type*} [AddCommGroup A]
example (x y z : A) : z + x + (y - z - x) = y := by
abel
end AdditiveGroups
group は群の公理から従う等式を解く tactic です.
加法可換群では abel が使えます.
どちらも「代数構造の公理に従う正規化」を行うものだと考えるとよいです.
準同型¶
群準同型や環準同型は,単なる関数ではなく,関数と保存性の証明をまとめた構造体です. このような表現を bundled map と呼びます.
モノイド準同型は M →* N,加法モノイド準同型は M →+ N,環準同型は R →+* S と書きます.
#check MonoidHom
#check AddMonoidHom
#check RingHom
section Homomorphisms
variable {M N P : Type*} [Monoid M] [Monoid N] [Monoid P]
example (f : M →* N) (x y : M) : f (x * y) = f x * f y := by
exact f.map_mul x y
example (f : M →* N) : f 1 = 1 := by
exact f.map_one
example (f : M →* N) (g : N →* P) : M →* P :=
g.comp f
variable {R S : Type*} [Ring R] [Ring S]
example (f : R →+* S) (x y : R) : f (x + y) = f x + f y := by
exact map_add f x y
example (f : R →+* S) (x y : R) : f (x * y) = f x * f y := by
exact map_mul f x y
example (f : R →+* S) : f 0 = 0 := by
exact map_zero f
end Homomorphisms
準同型 f : R →+* S は関数として使えます.
一方で,普通の関数合成 g ∘ f ではなく,構造を保った合成には comp を使います.
これは,合成後の写像が演算を保存することも一緒に記録する必要があるためです.
演習問題¶
以下の問題は,講義中または自習で by 以下を埋めることを想定しています.
まずは #check で使えそうな補題を探し,simp,group,abel,ring,ext を試してください.
モノイド準同型の合成が積を保つことを示してください.
example {M N P : Type*} [Monoid M] [Monoid N] [Monoid P]
(f : M →* N) (g : N →* P) (x y : M) :
(g.comp f) (x * y) = (g.comp f) x * (g.comp f) y := by
-- `map_mul` または `simp` を使う.
sorry
部分群¶
Subgroup G は G の部分群の型です.
これは Set G に「閉性の証明」を追加した bundled structure です.
そのため,x ∈ H のように集合として使えますが,同時に部分群としての構造も持っています.
#check Subgroup
#check AddSubgroup
section Subgroups
variable {G H : Type*} [Group G] [Group H]
variable (S T : Subgroup G)
example {x y : G} (hx : x ∈ S) (hy : y ∈ S) : x * y ∈ S := by
exact S.mul_mem hx hy
example {x : G} (hx : x ∈ S) : x⁻¹ ∈ S := by
exact S.inv_mem hx
example : ((S ⊓ T : Subgroup G) : Set G) = (S : Set G) ∩ (T : Set G) := by
rfl
example (x : G) : x ∈ (⊤ : Subgroup G) := by
trivial
example (x : G) : x ∈ (⊥ : Subgroup G) ↔ x = 1 := by
exact Subgroup.mem_bot
variable (f : G →* H) (U : Subgroup H)
example : Subgroup H :=
Subgroup.map f S
example : Subgroup G :=
Subgroup.comap f U
example (x : G) : x ∈ Subgroup.comap f U ↔ f x ∈ U := by
rfl
#check Subgroup.mem_map
#check MonoidHom.ker
#check MonoidHom.range
end Subgroups
部分群全体は包含関係で順序づけられ,束構造を持ちます.
S ⊓ T は交わりに対応します.
一方,S ⊔ T は単純な和集合ではなく,和集合で生成される部分群です.
これは「和集合は一般には部分群でない」ことを反映しています.
演習問題¶
-
部分群の
mapが包含を保つことを示してください. -
部分群の
comapが包含を保つことを示してください. -
MonoidHom.ker fの membership を読み替えてください. -
MonoidHom.range fの membership を読み替えてください. -
部分群の積で閉じていることを,
S.mul_memではなくshowでゴールを明示して証明してください.
環とイデアル¶
環論でも同じ設計が現れます.
Subring R は部分環,Ideal R はイデアルを表す bundled structure です.
可換環のイデアルでは,加法で閉じていて,外からの積で閉じていることを使います.
#check Subring
#check Ideal
#check RingEquiv
section Ideals
variable {R : Type*} [CommRing R]
variable (I J : Ideal R)
example {x y : R} (hx : x ∈ I) (hy : y ∈ I) : x + y ∈ I := by
exact I.add_mem hx hy
example {x : R} (hx : x ∈ I) (r : R) : r * x ∈ I := by
exact I.mul_mem_left r hx
example : ((I ⊓ J : Ideal R) : Set R) = (I : Set R) ∩ (J : Set R) := by
rfl
example (x : R) : x ∈ (⊥ : Ideal R) ↔ x = 0 := by
exact Ideal.mem_bot
#check Ideal.Quotient.mk
#check Ideal.Quotient.eq_zero_iff_mem
#check Ideal.map
#check Ideal.comap
end Ideals
Ideal.Quotient I は商環です.
商を扱うときは,代表元に依存しない定義であることを証明する必要があります.
このため,最初は #check で定義や補題の型を確認しながら進めるのが安全です.
演習問題¶
-
可換環のイデアル
I J : Ideal Rについて,I ⊓ Jの元であることを集合の交わりとして読み替えてください. -
可換環で,イデアルの元に外から掛けてもイデアルに入ることを左右両方で確認してください.
多項式と代数¶
Mathlib の多項式は Polynomial R です.
係数を埋め込む写像は Polynomial.C,不定元は Polynomial.X です.
#check Polynomial
#check Polynomial.C
#check Polynomial.X
section Polynomials
open Polynomial
variable {R : Type*} [Semiring R]
example : (X : Polynomial R) * C (1 : R) = X := by
simp
example (a : R) : (C a : Polynomial R) + 0 = C a := by
simp
end Polynomials
演習問題¶
多項式で,定数多項式の和が係数の和に対応することを示してください.
example {R : Type*} [Semiring R] (a b : R) :
(Polynomial.C a + Polynomial.C b : Polynomial R) = Polynomial.C (a + b) := by
-- `ext n` または `simp` を試す.
sorry
Algebra R A は,A が R 上の代数であることを表す型クラスです.
スカラー倍 r • a は,構造写像 algebraMap R A r による積として振る舞います.
#check Algebra
#check algebraMap
section Algebras
variable {R A : Type*} [CommSemiring R] [Semiring A] [Algebra R A]
example (r : R) (a : A) : r • a = algebraMap R A r * a := by
exact Algebra.smul_def r a
example (r s : R) : algebraMap R A (r + s) = algebraMap R A r + algebraMap R A s := by
exact map_add (algebraMap R A) r s
end Algebras
長めの例: ℤ を ℚ の加法部分群として作る¶
紙の数学では「整数全体は有理数の加法部分群である」と簡単に書きます.
Lean では,これを AddSubgroup ℚ の項として作ります.
ポイントは次の通りです.
- 台集合は
Set.range ((↑) : ℤ → ℚ)として表す. 0が入ることを示す.- 加法で閉じていることを示す.
- 負元で閉じていることを示す.
この例は,部分構造が「集合 + 閉性の証明」であることを具体的に示しています.
def integersInRationals : AddSubgroup ℚ where
carrier := Set.range ((↑) : ℤ → ℚ)
zero_mem' := by
use 0
norm_num
add_mem' := by
rintro _ _ ⟨m, rfl⟩ ⟨n, rfl⟩
use m + n
norm_num
neg_mem' := by
rintro _ ⟨m, rfl⟩
use -m
norm_num
example : (3 : ℚ) ∈ integersInRationals := by
use (3 : ℤ)
norm_num
example : (1 / 2 : ℚ) ∉ integersInRationals := by
rintro ⟨z, hz⟩
have htwo : ((2 * z : ℤ) : ℚ) = 1 := by
norm_num [Int.cast_mul, hz]
have hInt : (2 * z : ℤ) = 1 := by
exact_mod_cast htwo
omega
最後の例は少し人工的ですが,「Set.range の元である」という仮定を
∃ z : ℤ, ... として取り出し,整数性の情報を使って矛盾を出しています.
代数の形式化では,このように「集合としての記述」と「構造としての記述」を行き来することがよくあります.
長めの例: 共役部分群¶
群 G の部分群 S と元 g : G に対して,
g S g⁻¹ = {x | ∃ s ∈ S, x = g * s * g⁻¹} はまた部分群です.
これは抽象代数学の基本例で,閉性の証明に group がよく効きます.
section ConjugateSubgroup
variable {G : Type*} [Group G]
def conjugateSubgroup (g : G) (S : Subgroup G) : Subgroup G where
carrier := {x : G | ∃ s, s ∈ S ∧ x = g * s * g⁻¹}
one_mem' := by
refine ⟨1, S.one_mem, ?_⟩
group
mul_mem' := by
rintro x y ⟨s, hs, rfl⟩ ⟨t, ht, rfl⟩
refine ⟨s * t, S.mul_mem hs ht, ?_⟩
group
inv_mem' := by
rintro x ⟨s, hs, rfl⟩
refine ⟨s⁻¹, S.inv_mem hs, ?_⟩
group
example (g : G) (S : Subgroup G) {x : G} :
x ∈ conjugateSubgroup g S ↔ ∃ s, s ∈ S ∧ x = g * s * g⁻¹ := by
rfl
example (S : Subgroup G) {x : G} : x ∈ conjugateSubgroup (1 : G) S ↔ x ∈ S := by
constructor
· rintro ⟨s, hs, rfl⟩
simpa using hs
· intro hx
refine ⟨x, hx, ?_⟩
group
end ConjugateSubgroup
この例で使った証明パターンは,部分構造の自作で頻繁に現れます.
rintro ... ⟨s, hs, rfl⟩で存在記号と等式を分解する.- 閉性は
S.mul_mem,S.inv_memなどを使う. - 群の計算は
groupに任せる.
学部レベルの代数を形式化するときは,まずこのような「集合を carrier として持つ構造体」を自作できることが重要です.
演習問題¶
conjugateSubgroup について,g⁻¹ で再び共役すると元に戻ることを示してください.
example {G : Type*} [Group G] (g : G) (S : Subgroup G) :
conjugateSubgroup g⁻¹ (conjugateSubgroup g S) = S := by
-- `ext x` で部分群の等式を元ごとの同値にする.
-- その後,存在記号を分解して `group` を使う.
sorry
まとめ¶
代数の章で重要なのは,「構造」と「その構造を保つ写像」を型として読むことです.
Group G,Ring R,Module R M のような型クラスが演算や公理を供給し,
G →* H,R →+* S,Subgroup G,Ideal R のような bundled structure が数学的対象を表します.
証明では,手で公理を展開する前に,simp,group,abel,ring,rw,ext,#check を使って既存の構造を利用します.
形式化の tips¶
代数の形式化では,次の順に考えると進めやすくなります.
- 対象は型か,部分構造か,準同型かを決める.
- 演算が使えないときは,必要な型クラス仮定を探す.
- 部分構造の等式は
ext xで元ごとの同値にする. - membership は
simp,rfl,Subgroup.mem_mapなどで開く. - 群や環の計算は
group,abel,ringに任せる.
Chapter 02: 線形代数¶
Mathlib における基本的な線形代数を扱います.
参考: * Mathematics in Lean, 10. Linear Algebra: https://leanprover-community.github.io/mathematics_in_lean/C10_Linear_Algebra.html * Math in Lean: linear algebra: https://leanprover-community.github.io/theories/linear_algebra.html
主に次の対象が中心になります.
Module R M: 加法群や加法モノイドの上のスカラー倍構造V →ₗ[K] W: 線形写像Submodule K V: 部分空間・部分加群LinearIndependent,Basis,finrankMatrix m n R: 行列
Mathlib では,ベクトル空間は専用の VectorSpace 型クラスではなく,体 K 上の Module K V として扱います.
これは半環上の半加群や環上の加群まで同じ枠組みで扱うためです.
Module とスカラー倍¶
「K を体,V を K ベクトル空間とする」は,Lean では次のように書きます.
[AddCommGroup V] と [Module K V] を分けて仮定するのは,型クラス探索を安定させるためです.
スカラー倍は a • v と書き,補題名では smul と呼ばれます.
#check Module
#check SMul
#check smul_add
#check add_smul
#check smul_comm
section Modules
variable {K : Type*} [Field K]
variable {V : Type*} [AddCommGroup V] [Module K V]
example (a : K) (u v : V) : a • (u + v) = a • u + a • v := by
exact smul_add a u v
example (a b : K) (u : V) : (a + b) • u = a • u + b • u := by
exact add_smul a b u
example (a b : K) (u : V) : a • b • u = b • a • u := by
exact smul_comm a b u
example (u : V) : (0 : K) • u = 0 := by
simp
end Modules
module tactic は,加群の公理から従う等式を解くための tactic です.
ring や group と同じく,構造の公理に沿った正規化を行います.
section ModuleTactic
variable {K : Type*} [Field K]
variable {V : Type*} [AddCommGroup V] [Module K V]
example (a b : K) (x y : V) : a • (x + y) + b • x = (a + b) • x + a • y := by
module
end ModuleTactic
線形写像¶
V →ₗ[K] W は K 線形写像の型です.
これは関数と線形性の証明をまとめた bundled map です.
線形写像は関数として適用できますが,合成には LinearMap.comp または ∘ₗ を使います.
#check LinearMap
#check LinearEquiv
section LinearMaps
variable {K : Type*} [Field K]
variable {V W U : Type*}
variable [AddCommGroup V] [Module K V]
variable [AddCommGroup W] [Module K W]
variable [AddCommGroup U] [Module K U]
example (φ : V →ₗ[K] W) (a : K) (v : V) : φ (a • v) = a • φ v := by
exact map_smul φ a v
example (φ : V →ₗ[K] W) (v w : V) : φ (v + w) = φ v + φ w := by
exact map_add φ v w
example (φ : V →ₗ[K] W) (ψ : W →ₗ[K] U) : V →ₗ[K] U :=
ψ.comp φ
example (φ : V →ₗ[K] W) (ψ : W →ₗ[K] U) : V →ₗ[K] U :=
ψ ∘ₗ φ
example : V →ₗ[K] V :=
LinearMap.id
example (a : K) : V →ₗ[K] V :=
LinearMap.lsmul K V a
#check (LinearMap.lsmul K V : K →ₗ[K] V →ₗ[K] V)
end LinearMaps
線形写像どうしも加法やスカラー倍を持ちます. そのため,線形写像の空間そのものをベクトル空間として扱えます.
section LinearMapSpace
variable {K : Type*} [Field K]
variable {V W : Type*}
variable [AddCommGroup V] [Module K V]
variable [AddCommGroup W] [Module K W]
example (φ ψ : V →ₗ[K] W) (a : K) : V →ₗ[K] W :=
a • φ + ψ
example (φ ψ : V →ₗ[K] W) (v : V) : (φ + ψ) v = φ v + ψ v := by
rfl
end LinearMapSpace
演習問題¶
線形写像は 0 を 0 に送ることを示してください.
example {K V W : Type*} [Field K]
[AddCommGroup V] [Module K V]
[AddCommGroup W] [Module K W]
(f : V →ₗ[K] W) :
f 0 = 0 := by
-- `map_zero` または `simp`.
-- 解答例: exact map_zero f
sorry
部分空間・部分加群¶
Submodule K V は,V の K 部分空間,より一般には部分加群です.
Subgroup や Ideal と同じく,台集合と閉性の証明を持つ bundled structure です.
#check Submodule
#check Submodule.span
section Submodules
variable {K : Type*} [Field K]
variable {V W : Type*}
variable [AddCommGroup V] [Module K V]
variable [AddCommGroup W] [Module K W]
variable (S T : Submodule K V)
example {x y : V} (hx : x ∈ S) (hy : y ∈ S) : x + y ∈ S := by
exact S.add_mem hx hy
example {x : V} (a : K) (hx : x ∈ S) : a • x ∈ S := by
exact S.smul_mem a hx
example : ((S ⊓ T : Submodule K V) : Set V) = (S : Set V) ∩ (T : Set V) := by
rfl
example (x : V) : x ∈ (⊥ : Submodule K V) ↔ x = 0 := by
exact Submodule.mem_bot (R := K)
example (s : Set V) {x : V} (hx : x ∈ s) : x ∈ Submodule.span K s := by
exact Submodule.subset_span hx
variable (φ : V →ₗ[K] W) (U : Submodule K W)
example : Submodule K W :=
Submodule.map φ S
example : Submodule K V :=
Submodule.comap φ U
example (x : V) : x ∈ Submodule.comap φ U ↔ φ x ∈ U := by
rfl
#check LinearMap.ker
#check LinearMap.range
end Submodules
Submodule.span K s は集合 s : Set V で張られる部分空間です.
部分空間の像と逆像は,線形写像に沿って map と comap で表されます.
これは実践編 Chapter 01 の部分群と同じ設計です.
演習問題¶
-
kernel の元は,定義通り
f x = 0を満たすことを示してください. -
Submodule.mapとSubmodule.comapの membership を読み替えてください. -
線形写像の range が部分空間であることを確認してください.
一次独立・基底・次元¶
一次独立性は LinearIndependent K v で表します.
ここで v : ι → V は添字集合 ι で添字づけられたベクトルの族です.
基底は Basis ι K V で,添字集合 ι を持つ K 上の V の基底です.
#check LinearIndependent
#check Module.Basis
#check Module.rank
#check Module.finrank
section Dimension
variable {K : Type*} [Field K]
example : Module.finrank K (Fin 3 → K) = 3 := by
simp
example : Module.finrank K K = 1 := by
simp
end Dimension
Module.rank は基数値の次元です.
FiniteDimensional.finrank は自然数値の次元で,無限次元の場合は慣習的に 0 になります.
有限次元と分かっている状況では finrank が計算しやすいことが多いです.
行列¶
Matrix m n R は,行添字 m,列添字 n,成分型 R の行列です.
自然数ではなく任意の有限型で添字づけられる点が,紙の数学と少し違います.
Fin m を使うと,通常の m 行 n 列行列を表せます.
#check Matrix
#check Matrix.of
#check Matrix.det
#check Matrix.toLin
section Matrices
variable {K : Type*} [Field K]
example (A B : Matrix (Fin 2) (Fin 3) K) (i : Fin 2) (j : Fin 3) :
(A + B) i j = A i j + B i j := by
rfl
example (A : Matrix (Fin 2) (Fin 3) K) (i : Fin 3) (j : Fin 2) :
Aᵀ i j = A j i := by
rfl
example (A : Matrix (Fin 2) (Fin 3) K) (v : Fin 3 → K) (i : Fin 2) :
(A *ᵥ v) i = ∑ j, A i j * v j := by
rfl
example : Matrix.det (1 : Matrix (Fin 2) (Fin 2) K) = 1 := by
simp
end Matrices
行列は成分を計算したいときに便利です.
一方,抽象的な線形代数の証明では LinearMap を使う方が自然です.
基底を固定すると,線形写像と行列を対応させることができます.
演習問題¶
-
Matrix (Fin 2) (Fin 2) Kの単位行列の行列式が 1 であることを,simp以外の方法でも調べてください. -
行列と線形写像の橋渡しとして
Matrix.toLinの型を読み,どこで基底が必要になるか説明してください.
長めの例: K × K 上の線形写像と kernel¶
ここでは,2 次元ベクトル空間 K × K から K への線形写像
(x, y) ↦ x + y を作ります.
紙の数学では当たり前に線形写像と呼ぶものも,Lean では toFun,map_add',map_smul' を与えて構造体として作ります.
section CoordinateLinearMaps
variable {K : Type*} [Field K]
def sumPairLinear : (K × K) →ₗ[K] K where
toFun p := p.1 + p.2
map_add' := by
intro x y
simp [add_left_comm, add_comm]
map_smul' := by
intro a x
simp [mul_add]
example (x y : K) : sumPairLinear (x, y) = x + y := by
rfl
example (x y : K) :
(x, y) ∈ LinearMap.ker (sumPairLinear : (K × K) →ₗ[K] K) ↔ x + y = 0 := by
rfl
この kernel は,方程式 x + y = 0 で定まる直線です.
Lean では,kernel は Submodule K (K × K) です.
つまり,単なる集合ではなく,線形部分空間としての閉性も持っています.
#check LinearMap.ker
example (p q : K × K)
(hp : p ∈ LinearMap.ker (sumPairLinear : (K × K) →ₗ[K] K))
(hq : q ∈ LinearMap.ker (sumPairLinear : (K × K) →ₗ[K] K)) :
p + q ∈ LinearMap.ker (sumPairLinear : (K × K) →ₗ[K] K) := by
exact (LinearMap.ker sumPairLinear).add_mem hp hq
example (a : K) (p : K × K)
(hp : p ∈ LinearMap.ker (sumPairLinear : (K × K) →ₗ[K] K)) :
a • p ∈ LinearMap.ker (sumPairLinear : (K × K) →ₗ[K] K) := by
exact (LinearMap.ker sumPairLinear).smul_mem a hp
長めの例: 標準基底が K × K を張る¶
次に,e₁ = (1, 0) と e₂ = (0, 1) が K × K 全体を張ることを示します.
ここでは Basis を作るのではなく,まず Submodule.span だけを使います.
証明の中心は,任意の p : K × K について
を示すことです.
def planeE1 : K × K :=
(1, 0)
def planeE2 : K × K :=
(0, 1)
example : Submodule.span K ({planeE1, planeE2} : Set (K × K)) = ⊤ := by
ext p
constructor
· intro _
trivial
· intro _
have hp : p = p.1 • planeE1 + p.2 • planeE2 := by
ext <;> simp [planeE1, planeE2]
rw [hp]
apply Submodule.add_mem
· exact Submodule.smul_mem _ _ (Submodule.subset_span (by simp [planeE1]))
· exact Submodule.smul_mem _ _ (Submodule.subset_span (by simp [planeE2]))
end CoordinateLinearMaps
この例では ext p で部分空間の等式を元ごとの同値に変換しています.
その後,⊤ への所属は自明なので,全ての p が左辺の span に入ることを示せば十分です.
このような proof は,線形代数の形式化で頻繁に使う基本パターンです.
- span に生成元が入る:
Submodule.subset_span - span がスカラー倍で閉じる:
Submodule.smul_mem - span が和で閉じる:
Submodule.add_mem - 座標計算:
extとsimp
演習問題¶
-
K × K上の線形写像(x, y) ↦ x - yをLinearMapとして作ってください. -
sumPairLinearの kernel がx + y = 0であることを,rflではなくsimpで証明してください. -
planeE1とplaneE2が一次独立であることを示してください. これは少し難しい問題です.まず#check LinearIndependentで statement の形を確認してください.example {K : Type*} [Field K] : LinearIndependent K (fun i : Fin 2 => if i = 0 then (planeE1 : K × K) else planeE2) := by -- 方針: `linearIndependent_iff` 系の補題を探す. -- 解答例: -- rw [linearIndependent_iff] -- intro s hzero -- ext i -- fin_cases i -- · have h1 := congrArg Prod.fst hzero -- simp [Finsupp.linearCombination, planeE1, planeE2, Finsupp.sum_fintype] at h1 -- simpa using h1 -- · have h2 := congrArg Prod.snd hzero -- simp [Finsupp.linearCombination, planeE1, planeE2, Finsupp.sum_fintype] at h2 -- simpa using h2 sorry -
K × Kの任意の点をplaneE1とplaneE2の線形結合として書いてください.
まとめ¶
Mathlib の線形代数では,ベクトル空間を Module K V として読みます.
線形写像 V →ₗ[K] W,部分空間 Submodule K V,行列 Matrix m n R は,いずれも数学的な構造とその公理を bundled structure として持ちます.
証明を書くときは,まず #check で補題の型を確認し,simp,module,rw,ext,linear_combination などを必要に応じて使います.
形式化の tips¶
線形代数の形式化では,紙の証明で省略している「どの空間の元か」を Lean に明示する必要があります.
- 体
Kとベクトル空間Vを分ける. - ベクトル空間は
[AddCommGroup V] [Module K V]として仮定する. - 線形写像は関数ではなく
V →ₗ[K] Wとして扱う. - 部分空間は
Submodule K Vであり,集合として使うときは coercion が働く. - span の証明では,生成元が span に入ることと,和・スカラー倍で閉じることを使う.
Chapter 03: 位相¶
Mathlib における基本的な位相を扱います.
参考:
- Mathematics in Lean, 11. Topology: https://leanprover-community.github.io/mathematics_in_lean/C11_Topology.html
- Maths in Lean: Topological, uniform and metric spaces: https://leanprover-community.github.io/theories/topology.html
フィルター,距離空間,位相空間,連続性,コンパクト性などが説明されています.
Lean での位相はフィルターに基づいて形式化されています. フィルターは,数列の極限,関数の極限,無限遠での振る舞い,近傍などを同じ言葉で扱うための道具です.
位相空間¶
位相空間構造は TopologicalSpace X です.
開集合は IsOpen U,閉集合は IsClosed C と書きます.
interior,closure,frontier なども Set X に対する操作です.
Mathlib では,位相空間構造の中に開集合を表す述語が入っています.
IsOpen はその述語を取り出したものです.
一方,IsClosed C は独立した原始概念ではなく,補集合 Cᶜ が開集合であることとして定義されています.
class TopologicalSpace (X : Type u) where
protected IsOpen : Set X → Prop
protected isOpen_univ : IsOpen univ
protected isOpen_inter : ∀ s t, IsOpen s → IsOpen t → IsOpen (s ∩ t)
protected isOpen_sUnion : ∀ s, (∀ t ∈ s, IsOpen t) → IsOpen (⋃₀ s)
def IsOpen : Set X → Prop := TopologicalSpace.IsOpen
class IsClosed (s : Set X) : Prop where
isOpen_compl : IsOpen sᶜ
したがって,閉集合に関する主張は,補集合を取ると開集合に関する主張として読めます.
#check TopologicalSpace
#check IsOpen
#check IsClosed
#check interior
#check closure
#check frontier
section TopologicalSpaces
variable {X : Type*} [TopologicalSpace X]
variable {U V C D Y Z : Set X}
example (hU : IsOpen U) (hV : IsOpen V) : IsOpen (U ∩ V) := by
exact hU.inter hV
example (hC : IsClosed C) (hD : IsClosed D) : IsClosed (C ∪ D) := by
exact hC.union hD
example : IsOpen Cᶜ ↔ IsClosed C := by
exact isOpen_compl_iff
example : interior Y = Y ↔ IsOpen Y := by
exact interior_eq_iff_isOpen
example (hYZ : Y ⊆ Z) : interior Y ⊆ interior Z := by
exact interior_mono hYZ
example : closure Y = Y ↔ IsClosed Y := by
exact closure_eq_iff_isClosed
example (hYZ : Y ⊆ Z) : closure Y ⊆ closure Z := by
exact closure_mono hYZ
end TopologicalSpaces
集合の等式や包含と同じように,位相的な操作も Set の補題と組み合わせて使います.
たとえば,closure_mono は集合の包含から閉包の包含を得る補題です.
演習問題¶
-
closure_monoを使って,s ⊆ tならclosure s ⊆ closure tを示してください. -
interior s ⊆ sとs ⊆ closure sをそれぞれ確認してください.
フィルター¶
Filter α は,α の部分集合のうち,どれをフィルターに含めるかを指定する構造です.
数学的には,集合 \(X\) 上のフィルター \(\mathcal F\) は,部分集合族
\(\mathcal F \subseteq \mathcal P(X)\) であって,次を満たすものです.
通常の数学ではさらに \(\emptyset \notin \mathcal F\) も仮定します.
Lean の Filter X ではこの条件は定義には入っていませんが,
この章で扱う atTop や 𝓝 x を読むうえでは,まず意識しなくてかまいません.
この章では,フィルターの一般論よりも,次の 3 つの読み方が重要です.
型 α 上のフィルター l : Filter α を考えます.
s ∈ l:s : Set αはαの部分集合であり,sがフィルターlに含まれる,という意味です.∀ᶠ x in l, p x:x : αは集合ではなく,αの点を表す束縛変数です.p : α → Propに対して,p xが成り立つような点x全体の集合が, フィルターlに含まれる,という意味です. 定義上は{x : α | p x} ∈ lという主張です.Tendsto f l m:x : αがlに沿って動くとき,f xがmに沿って動く.
数式で書くと,∀ᶠ x in l, p x は
という条件です.
また,写像 \(f : X \to Y\) とフィルター \(\mathcal F\),\(\mathcal G\) について
Tendsto f l m は,次の条件に対応します.
この章で主に使うフィルターは,atTop と 𝓝 x です.
atTop は,順序集合の「上の方へ行く」ことを表すフィルターです.
自然数上では「十分大きい n」を意味します.
自然数上の部分集合 \(S \subseteq \mathbb N\) については,
と読めます.
この講義では,次の補題を atTop の定義のように読めば十分です.
lemma mem_atTop_sets :
s ∈ (atTop : Filter α) ↔ ∃ a : α, ∀ b ≥ a, b ∈ s
lemma eventually_atTop :
(∀ᶠ x in atTop, p x) ↔ ∃ a, ∀ b ≥ a, p b
特に自然数列の命題 P : ℕ → Prop について,∀ᶠ m in atTop, P m は
「ある n が存在して,n ≤ m なる任意の m について P m」という意味です.
数式で書けば,
です.
つまり,有限個の初期項を除けば P m が常に成り立つ,ということです.
一方,𝓝 x は点 x の近傍フィルターです.
s ∈ 𝓝 x は,s が x の近傍であることを意味します.
位相空間 \(X\) の点 \(x\) と部分集合 \(S \subseteq X\) については,
実際には,次の補題で読むのが便利です.
つまり,s が x の近傍であるとは,s の中に x を含む開集合 t があることです.
Tendsto f l m は,「x がフィルター l に沿って動くとき,f x がフィルター m に沿って動く」という意味です.
数列の極限も,関数の一点での極限も,無限遠での極限も,この形で表されます.
内部的には map f l ≤ m という定義ですが,まずは
「m に含まれる任意の集合の逆像は,l に含まれる」と読むのがよいです.
#check Filter
#check Tendsto
#check atTop
#check 𝓝
#check Filter.Eventually
#check mem_atTop_sets
#check eventually_atTop
#check mem_nhds_iff
section Filters
example {α : Type*} {l : Filter α} {p : α → Prop} :
(∀ᶠ x in l, p x) = ({x | p x} ∈ l) := by
rfl
example : (∀ᶠ n in (atTop : Filter Nat), 3 ≤ n) := by
exact eventually_ge_atTop 3
example {P : ℕ → Prop} :
(∀ᶠ m in (atTop : Filter ℕ), P m) ↔ ∃ n, ∀ m, n ≤ m → P m := by
exact eventually_atTop
example {s : Set ℕ} :
s ∈ (atTop : Filter ℕ) ↔ ∃ n, ∀ m, n ≤ m → m ∈ s := by
exact mem_atTop_sets
example {X : Type*} [TopologicalSpace X] {x : X} {s : Set X} :
s ∈ 𝓝 x ↔ ∃ t ⊆ s, IsOpen t ∧ x ∈ t := by
exact mem_nhds_iff
example {α β : Type*} {f : α → β} {l : Filter α} {m : Filter β} :
Tendsto f l m = (map f l ≤ m) := by
rfl
end Filters
近傍フィルターで収束と連続を読む¶
この節で使う対応は次の 2 つです.
すなわち
また,写像 \(f : X \to Y\) について
点列 u : ℕ → X が x に収束することは,次のように書きます.
これは数式では,
です. さらに,\(\mathrm{atTop}\) を
と読むと,これは通常の点列収束
と同じです.
tendsto_nhds は,この条件を開集合で読むための補題です.
theorem tendsto_nhds {a : X} {f : α → X} {l : Filter α} :
Tendsto f l (𝓝 a) ↔
∀ s, IsOpen s → a ∈ s → f ⁻¹' s ∈ l
対応する証明は,次の式変形として読めます.
まず \(U\) が \(x\) を含む開集合なら
したがって
逆向きでは,任意の \(A \in \mathcal N(x)\) に対して
仮定より
\(U \subseteq A\) なので
すなわち
一点 x での連続性も,同じ Tendsto で定義されます.
数式では,
です. 開集合で書けば,
大域的な連続性は,次の補題により「開集合の逆像が開集合」と同値になります. すなわち,
theorem continuous_def :
Continuous f ↔ ∀ s, IsOpen s → IsOpen (f ⁻¹' s)
theorem continuous_iff_continuousAt :
Continuous f ↔ ∀ x, ContinuousAt f x
この同値性の証明も,近傍の式で追えます. 各点で
が成り立つとします. \(V \subseteq Y\) が開集合なら,
よって
任意の点 \(x \in f^{-1}(V)\) で \(f^{-1}(V)\) が \(x\) の近傍なので,
逆に,
を仮定します. \(A \in \mathcal N(f(x))\) なら
このとき
したがって
これは
です.
#check tendsto_nhds
#check continuous_def
#check continuous_iff_continuousAt
section NeighborhoodFilters
example {X : Type*} [TopologicalSpace X] (x : X) : Filter X :=
𝓝 x
example {X : Type*} [TopologicalSpace X] {u : ℕ → X} {U : Set X} :
(u ⁻¹' U ∈ atTop) = (∀ᶠ n in atTop, u n ∈ U) := by
rfl
example {X : Type*} [TopologicalSpace X] (u : ℕ → X) (x : X) :
Tendsto u atTop (𝓝 x) ↔
∀ U : Set X, IsOpen U → x ∈ U → ∀ᶠ n in atTop, u n ∈ U := by
exact tendsto_nhds
example {X Y : Type*} [TopologicalSpace X] [TopologicalSpace Y]
{f : X → Y} {x : X} :
ContinuousAt f x = Tendsto f (𝓝 x) (𝓝 (f x)) := by
rfl
example {X Y : Type*} [TopologicalSpace X] [TopologicalSpace Y]
{f : X → Y} :
Continuous f ↔ ∀ x, Tendsto f (𝓝 x) (𝓝 (f x)) := by
simpa [ContinuousAt] using (continuous_iff_continuousAt (f := f))
example {X Y : Type*} [TopologicalSpace X] [TopologicalSpace Y]
{f : X → Y} :
Continuous f ↔ ∀ U : Set Y, IsOpen U → IsOpen (f ⁻¹' U) := by
exact continuous_def
example {X Y : Type*} [TopologicalSpace X] [TopologicalSpace Y]
{f : X → Y} :
(∀ x, Tendsto f (𝓝 x) (𝓝 (f x))) ↔
∀ U : Set Y, IsOpen U → IsOpen (f ⁻¹' U) := by
rw [← continuous_def]
simpa [ContinuousAt] using (continuous_iff_continuousAt (f := f)).symm
end NeighborhoodFilters
演習問題¶
Tendsto の定義を map と filter の順序として読み替えてください.
example {α β : Type*} (f : α → β) (l : Filter α) (m : Filter β) :
Tendsto f l m = (map f l ≤ m) := by
-- 定義そのもの.
-- 解答例: rfl
sorry
連続性の基本 API¶
前節では,Tendsto,𝓝 x,開集合の逆像による連続性を数式で読みました.
ここからは,それらを Lean で使うときの名前を確認します.
関数 f : X → Y の大域的な連続性は Continuous f,
点 x での連続性は ContinuousAt f x,
集合 s 上での連続性は ContinuousOn f s です.
この節では定義をもう一度展開するのではなく,よく使う読み替えと操作だけを確認します.
rfl:ContinuousAt f xをTendsto f (𝓝 x) (𝓝 (f x))と見る.continuousAt_def: 点での連続性を近傍の逆像条件として見る.tendsto_nhds: 終域側が𝓝 yのTendstoを開集合で読む.hf.continuousAt: 大域的連続性から点での連続性を得る.hg.comp hf: 連続写像の合成が連続であることを使う.
ここで Continuous.comp は,Continuous の field ではなく,
連続写像の合成が連続であることを述べる定理です.
hg.comp hf のように書くとメソッドのように見えますが,
Lean では名前空間 Continuous にある定理をドット notation で適用している,と読むとよいです.
一方,Continuous の定義の中で宣言されている成分,たとえば開集合の逆像が開集合であることを表す成分は field です.
#check Continuous
#check ContinuousAt
#check ContinuousOn
#check continuousAt_def
#check tendsto_nhds
#check Continuous.isOpen_preimage
#check Continuous.comp
section Continuity
variable {X Y Z : Type*}
variable [TopologicalSpace X] [TopologicalSpace Y] [TopologicalSpace Z]
example {f : X → Y} {x : X} :
ContinuousAt f x = Tendsto f (𝓝 x) (𝓝 (f x)) := by
rfl
example {f : X → Y} {x : X} :
ContinuousAt f x ↔ ∀ A ∈ 𝓝 (f x), f ⁻¹' A ∈ 𝓝 x := by
exact continuousAt_def
example {α : Type*} {f : α → Y} {l : Filter α} {y : Y} :
Tendsto f l (𝓝 y) ↔ ∀ s : Set Y, IsOpen s → y ∈ s → f ⁻¹' s ∈ l := by
exact tendsto_nhds
example {f : X → Y} (hf : Continuous f) (x : X) : ContinuousAt f x := by
exact hf.continuousAt
example {f : X → Y} {g : Y → Z} (hf : Continuous f) (hg : Continuous g) :
Continuous fun x => g (f x) := by
exact hg.comp hf
end Continuity
具体的な極限計算¶
実数列 \(a_n\) が \(+\infty\) に発散することは,Lean では
と書きます.
たとえば,自然数を実数に埋め込んだ列 \(n \mapsto n\) は atTop に収束します.
具体的な極限計算では,次の 2 つを分けると読みやすくなります.
Continuous.tendstoなどで,まずTendstoの証明を作る.- 終点の値の計算は
calcで段階的に書く.
calc は,等式や不等式を紙の計算に近い形で並べるための構文です.
Tendsto そのものを calc で示すというより,
𝓝 ((3 : ℝ)^2 + 1) を 𝓝 10 に直すような小さな計算に使うと効果的です.
section ConcreteLimits
example : Tendsto (fun n : ℕ => (n : ℝ)) atTop atTop := by
exact tendsto_natCast_atTop_atTop
一点での関数の極限は,始域側を 𝓝 a,終域側を 𝓝 b として
と書きます.
以下は \(\lim_{x \to 2} (x+3) = 5\) です.
Continuous.tendsto は,連続性からその点での極限を取り出す補題です.
最後に,2 + 3 = 5 という数値計算を calc で明示します.
example : Tendsto (fun x : ℝ => x + 3) (𝓝 2) (𝓝 5) := by
have hlim :
Tendsto (fun x : ℝ => x + 3) (𝓝 2) (𝓝 ((2 : ℝ) + 3)) :=
((by continuity : Continuous fun x : ℝ => x + 3).tendsto (2 : ℝ))
have hval : (2 : ℝ) + 3 = 5 := by
calc
(2 : ℝ) + 3 = (5 : ℝ) := by norm_num
_ = 5 := by rfl
simpa [hval] using hlim
上の例では,連続性から得られる極限はまず
という形です.
一方,示したいゴールは終域側が 𝓝 5 です.
そこで,まずこの極限を hlim として保存し,値の計算
を hval として別に示します.
最後の simpa [hval] using hlim は,hlim の終域側の 𝓝 (2 + 3) を
𝓝 5 に書き換えています.
同じ方針で,多項式や初等関数の極限も計算できます. 次は \(\lim_{x \to 3} (x^2+1) = 10\) と \(\lim_{x \to 0}(\sin x + x^2)=0\) です.
example : Tendsto (fun x : ℝ => x ^ 2 + 1) (𝓝 3) (𝓝 10) := by
have hlim :
Tendsto (fun x : ℝ => x ^ 2 + 1) (𝓝 3) (𝓝 ((3 : ℝ) ^ 2 + 1)) :=
((by continuity : Continuous fun x : ℝ => x ^ 2 + 1).tendsto (3 : ℝ))
have hval : (3 : ℝ) ^ 2 + 1 = 10 := by
calc
(3 : ℝ) ^ 2 + 1 = (9 : ℝ) + 1 := by norm_num
_ = 10 := by norm_num
simpa [hval] using hlim
収束先の値を明示しない方法¶
収束先の値をまだ計算したくないときは,𝓝 10 のように数値を明示せず,
𝓝 (f a) の形で書けます.
たとえば次の例は,\(\lim_{x \to 3}(x^2+1)\) の値を 10 まで計算せず,
単に「点 3 における関数値」へ収束する,という形で述べています.
example :
Tendsto (fun x : ℝ => x ^ 2 + 1) (𝓝 3)
(𝓝 ((fun x : ℝ => x ^ 2 + 1) 3)) := by
exact ((by continuity : Continuous fun x : ℝ => x ^ 2 + 1).tendsto (3 : ℝ))
より一般に,収束先の値そのものを命題の外に出したいなら, 存在命題として
と書けます.
この場合,証明の中では refine ⟨..., ?_⟩ によって候補となる値を与えます.
example :
∃ y : ℝ, Tendsto (fun x : ℝ => x ^ 2 + 1) (𝓝 3) (𝓝 y) := by
refine ⟨(fun x : ℝ => x ^ 2 + 1) 3, ?_⟩
exact ((by continuity : Continuous fun x : ℝ => x ^ 2 + 1).tendsto (3 : ℝ))
一方で,statement に直接 𝓝 _ と書いて収束先を完全に空欄にする方法は,
通常はうまくいきません.
-- これは基本的には失敗します.
-- example : Tendsto (fun x : ℝ => x ^ 2 + 1) (𝓝 3) (𝓝 _) := by
-- exact ((by continuity : Continuous fun x : ℝ => x ^ 2 + 1).tendsto (3 : ℝ))
example : ... の型は証明本体を読む前に確定されるため,
証明本体から _ の中身を推論することはできないからです.
値を計算したくない場合は,まず 𝓝 (f a) の形で書くのが実用的です.
example : Tendsto (fun x : ℝ => Real.sin x + x ^ 2) (𝓝 0) (𝓝 0) := by
simpa using ((by continuity : Continuous fun x : ℝ => Real.sin x + x ^ 2).tendsto (0 : ℝ))
無限遠での極限も同じ Tendsto です.
次は \(\lim_{x \to +\infty} 1/x = 0\) です.
ここでは始域側のフィルターが atTop,終域側のフィルターが 𝓝 0 になっています.
example : Tendsto (fun x : ℝ => 1 / x) atTop (𝓝 0) := by
simpa [one_div] using
(tendsto_inv_atTop_zero : Tendsto (fun x : ℝ => x⁻¹) atTop (𝓝 0))
end ConcreteLimits
具体的な連続性計算¶
実数上の初等関数の連続性は,continuity tactic で証明できることが多いです.
失敗した場合は,Continuous.add,Continuous.mul,Continuous.comp などの補題を明示的に使います.
式が長くなる場合は,1 行で全部を書くよりも,中間事実に名前を付けると読みやすくなります.
たとえば分数関数では,分子の連続性,分母の連続性,分母が 0 でないことを別々の have にします.
数値計算や式変形が長い場合は,前節と同じようにその部分だけ calc に切り出せます.
section ConcreteContinuity
example : Continuous fun x : ℝ => x ^ 2 + 1 := by
continuity
example : ContinuousAt (fun x : ℝ => Real.sin x + x ^ 2) (0 : ℝ) := by
simpa using
(Real.continuous_sin.continuousAt.add
((continuousAt_id : ContinuousAt (fun x : ℝ => x) 0).pow 2))
上の ContinuousAt の例では,Real.continuous_sin,continuousAt_id,
.pow,.add を手で組み合わせています.
大域的な連続性なら,同じ組み合わせを continuity に任せられることも多いです.
たとえば,\(x \mapsto \sin x + x^2\) は次のように証明できます.
分母を持つ関数では,分母が 0 でないことが必要です. 一点での連続性なら,その点で分母が 0 でないことを示せば十分です. 次は \(x=0\) における
の連続性です.
example : ContinuousAt (fun x : ℝ => (x ^ 2 + 1) / (x + 3)) 0 := by
have hnum : ContinuousAt (fun x : ℝ => x ^ 2 + 1) 0 :=
((continuousAt_id : ContinuousAt (fun x : ℝ => x) 0).pow 2).add continuousAt_const
have hden : ContinuousAt (fun x : ℝ => x + 3) 0 :=
(continuousAt_id : ContinuousAt (fun x : ℝ => x) 0).add continuousAt_const
have hden0 : (0 : ℝ) + 3 ≠ 0 := by
norm_num
exact hnum.div hden hden0
大域的な連続性では,すべての点で分母が 0 でないことを示します. 次の例では \(x^2+2>0\) なので分母は 0 になりません.
example : Continuous fun x : ℝ => (x ^ 2 + 1) / (x ^ 2 + 2) := by
have hnum : Continuous fun x : ℝ => x ^ 2 + 1 :=
((continuous_id : Continuous fun x : ℝ => x).pow 2).add continuous_const
have hden : Continuous fun x : ℝ => x ^ 2 + 2 :=
((continuous_id : Continuous fun x : ℝ => x).pow 2).add continuous_const
have hden0 : ∀ x : ℝ, x ^ 2 + 2 ≠ 0 := by
intro x
positivity
exact hnum.div hden hden0
end ConcreteContinuity
演習問題¶
-
開集合の連続写像による逆像が開集合であることを示してください.
-
閉集合の連続写像による逆像が閉集合であることを示してください.
-
実数値連続関数
f g : X → ℝについて,集合{x | f x ≤ g x}が閉集合であることを示してください. -
{x | f x < g x}が開集合であることを調べてください. -
ContinuousAtがTendstoであることを確認してください.
距離空間での読み替え¶
距離空間や擬距離空間では,dist x y,開球 Metric.ball x ε,閉球 Metric.closedBall x ε などを使います.
距離空間は位相空間構造を誘導するため,距離の話から IsOpen や Continuous の話へ移れます.
また ContinuousAt のフィルターによる定義は,距離空間では通常の ε-δ 条件と同値になります.
#check PseudoMetricSpace
#check Metric.ball
#check Metric.closedBall
#check dist
#check Metric.continuousAt_iff
section MetricSpaces
open Metric
variable {X : Type*} [PseudoMetricSpace X]
example (x : X) : dist x x = 0 := by
simp
example (x y : X) : 0 ≤ dist x y := by
exact dist_nonneg
example (x : X) (ε : ℝ) (hε : 0 < ε) : x ∈ Metric.ball x ε := by
simpa [Metric.mem_ball] using hε
example (x : X) (ε : ℝ) : IsOpen (Metric.ball x ε) := by
exact Metric.isOpen_ball
end MetricSpaces
section MetricContinuity
example {f : ℝ → ℝ} {a : ℝ} :
ContinuousAt f a ↔ ∀ ε > 0, ∃ δ > 0, ∀ ⦃x : ℝ⦄,
dist x a < δ → dist (f x) (f a) < ε := by
exact Metric.continuousAt_iff
example : ∀ ε > 0, ∃ δ > 0, ∀ ⦃x : ℝ⦄,
dist x 3 < δ → dist (x + 1) ((3 : ℝ) + 1) < ε := by
have h : ContinuousAt (fun y : ℝ => y + 1) (3 : ℝ) := by
exact (continuousAt_id : ContinuousAt (fun y : ℝ => y) 3).add continuousAt_const
exact Metric.continuousAt_iff.mp h
end MetricContinuity
Metric.ball x ε は集合です.
したがって x ∈ Metric.ball x ε や IsOpen (Metric.ball x ε) のように,集合に関する命題として扱えます.
Metric.continuousAt_iff を使うと,具体的な関数の連続性から
ε-δ 条件を取り出せます.
上の例は \(x \mapsto x+1\) が \(x=3\) で連続であることを,
距離による条件として読み替えています.
ここに現れる ∀ ⦃x : ℝ⦄, ... の ⦃x : ℝ⦄ は strict implicit binder です.
x 自体は普通の実数の点ですが,定理を使うときには明示的に渡さず,
後続の仮定 dist x a < δ などから Lean が推論します.
たとえば h : ∀ ⦃x : ℝ⦄, dist x a < δ → ... があるとき,
h hx のように書くと,hx の型から x が推論されます.
構文としては {{x : ℝ}} と入力でき,表示上は ⦃x : ℝ⦄ になります.
括弧の種類の一般的な説明は基礎編 Chapter 02 の variable と引数の節で扱いました.
演習問題¶
-
距離空間で,開球が開集合であることをもう一度証明してください.
-
距離空間で,
ContinuousAtがε-δ条件と同値であることを確認してください.
コンパクト性と連結性¶
ここまでの IsOpen,IsClosed,Continuous を使って,位相的な性質を扱います.
コンパクト性は IsCompact s で表されます.
これは s : Set X に対する述語です.
閉集合や連続写像との相互作用は,解析や微分積分で頻繁に使います.
連結性について,Mathlib では空集合も許した連結性を IsPreconnected s と呼びます.
非空性まで含めた通常の連結集合は IsConnected s です.
中間値の定理の証明で本質的に使うのは「2 つの閉集合による分離ができない」という性質なので,
IsPreconnected が主役になります.
#check IsCompact
#check IsCompact.image
#check IsClosed
#check IsPreconnected
#check IsConnected
#check isPreconnected_Icc
#check IsPreconnected.intermediate_value
#check intermediate_value_Icc
#check intermediate_value_Icc'
section CompactnessAndConnectedness
variable {X Y : Type*} [TopologicalSpace X] [TopologicalSpace Y]
variable {s : Set X} {f : X → Y}
example (hs : IsCompact s) (hf : Continuous f) : IsCompact (f '' s) := by
exact hs.image hf
example : IsPreconnected (Icc (0 : ℝ) 1) := by
exact isPreconnected_Icc
end CompactnessAndConnectedness
IsCompact.image は,コンパクト集合の連続像がコンパクトであることを述べます.
isPreconnected_Icc は,閉区間 Icc a b が preconnected であるという定理です.
IsPreconnected.intermediate_value は,preconnected な集合上の連続関数について,
端点値の間の閉区間が像に含まれることを述べます.
閉区間に特化した使いやすい形が intermediate_value_Icc です.
長めの例: 連続関数の零点集合は閉集合¶
位相の典型的な主張として,連続関数 f : X → ℝ の零点集合
は閉集合です.
紙の証明では「{0} は ℝ の閉集合で,零点集合はその逆像である」と説明します.
Lean でも同じ構造で証明します.
def zeroSet {X : Type*} (f : X → ℝ) : Set X :=
{x | f x = 0}
section ZeroSet
variable {X : Type*} [TopologicalSpace X]
variable {f : X → ℝ}
example (hf : Continuous f) : IsClosed (zeroSet f) := by
simpa [zeroSet] using isClosed_singleton.preimage hf
example (hf : Continuous f) (c : ℝ) : IsClosed {x : X | f x = c} := by
simpa using isClosed_eq hf continuous_const
end ZeroSet
2 つ目の例は,level set {x | f x = c} が閉集合であることを示しています.
証明では {x | f x = c} を {x | f x - c = 0} と見て,
連続関数 x ↦ f x - c の零点集合として扱っています.
simpa は,このような表現の差を整理するためによく使います.
演習問題¶
zeroSet を使わずに {x | f x = 0} が閉集合であることを直接証明してください.
example {X : Type*} [TopologicalSpace X] {f : X → ℝ} (hf : Continuous f) :
IsClosed {x : X | f x = 0} := by
-- `isClosed_eq hf continuous_const` を使う.
-- 解答例: simpa using isClosed_eq hf continuous_const
sorry
長めの例: コンパクト集合の連続像¶
コンパクト集合の連続像はコンパクトです. さらに,値域が Hausdorff 空間ならコンパクト集合は閉集合です. したがって,Hausdorff 空間への連続写像では,コンパクト集合の像は閉集合になります.
section CompactImage
variable {X Y : Type*} [TopologicalSpace X] [TopologicalSpace Y] [T2Space Y]
variable {s : Set X} {f : X → Y}
example (hs : IsCompact s) (hf : Continuous f) : IsClosed (f '' s) := by
exact (hs.image hf).isClosed
end CompactImage
ここで [T2Space Y] は,Y が Hausdorff 空間であるという型クラス仮定です.
定理 IsCompact.isClosed は一般の位相空間では成り立たず,Hausdorff 条件を要求します.
型クラス仮定として必要な位相的条件が明示されるのは,Mathlib の位相の読み方で重要です.
長めの例: 中間値の定理¶
位相のもう 1 つの典型例として,中間値の定理を見ます.
紙の数学では「閉区間 \([a,b]\) は連結であり,連続写像は連結集合を連結集合へ送る. したがって実数値連続関数の像は区間になり,端点値の間の値をすべて取る」と説明します. 数式で書けば,連結な集合 \(s\) 上の連続関数 \(f : X \to \mathbb R\) について, \(a,b \in s\) かつ \(f(a) \le f(b)\) なら
です. つまり,
閉区間版では,\(a \le b\) かつ \(f\) が \([a,b]\) 上連続であれば,
となります.
端点値の大小が逆なら,\(f(b) \le c \le f(a)\) の形で同じ主張を使います.
前の節で見た IsPreconnected と intermediate_value_Icc を使うと,
この議論をそのまま Lean で表せます.
section IntermediateValueTheorem
example {X : Type*} [TopologicalSpace X] {s : Set X} (hs : IsPreconnected s)
{a b : X} (ha : a ∈ s) (hb : b ∈ s) {f : X → ℝ}
(hf : ContinuousOn f s) :
Icc (f a) (f b) ⊆ f '' s := by
exact hs.intermediate_value ha hb hf
example {X : Type*} [TopologicalSpace X] {s : Set X} (hs : IsPreconnected s)
{a b : X} (ha : a ∈ s) (hb : b ∈ s) {f : X → ℝ}
(hf : ContinuousOn f s) {c : ℝ} (hc : c ∈ Icc (f a) (f b)) :
∃ x ∈ s, f x = c := by
rcases hs.intermediate_value ha hb hf hc with ⟨x, hx, hfx⟩
exact ⟨x, hx, hfx⟩
example {f : ℝ → ℝ} {a b c : ℝ}
(hab : a ≤ b) (hf : ContinuousOn f (Icc a b))
(ha : f a ≤ c) (hb : c ≤ f b) :
∃ x ∈ Icc a b, f x = c := by
exact intermediate_value_Icc hab hf ⟨ha, hb⟩
example {f : ℝ → ℝ} {a b c : ℝ}
(hab : a ≤ b) (hf : ContinuousOn f (Icc a b))
(ha : f b ≤ c) (hb : c ≤ f a) :
∃ x ∈ Icc a b, f x = c := by
exact intermediate_value_Icc' hab hf ⟨ha, hb⟩
end IntermediateValueTheorem
最後の 2 つの例は,端点値の順序に応じて定理を使い分けています.
intermediate_value_Icc は f a ≤ c ≤ f b の場合,
intermediate_value_Icc' は f b ≤ c ≤ f a の場合です.
具体例として,\(x^2\) が \([0,2]\) 上で値 \(2\) を取ることを示します. これは平方根の存在そのものではありませんが,中間値の定理の使い方としては典型的です. ここで使っている数学的な事実は,
です.
example : ∃ x ∈ Icc (0 : ℝ) 2, x ^ 2 = 2 := by
have hcont : ContinuousOn (fun x : ℝ => x ^ 2) (Icc (0 : ℝ) 2) := by
exact ((continuous_id : Continuous fun x : ℝ => x).pow 2).continuousOn
have h2 : (2 : ℝ) ∈ Icc ((fun x : ℝ => x ^ 2) 0) ((fun x : ℝ => x ^ 2) 2) := by
norm_num
simpa using intermediate_value_Icc (by norm_num : (0 : ℝ) ≤ 2) hcont h2
Mathlib の証明の読み方¶
Mathlib の本体では,まず次のような少し強い形を証明しています.
数学的には,連続関数 \(f,g : X \to \mathbb R\) と点 \(a,b \in X\) について
が成り立つなら,
を示す形です.
証明の核だけを抜き出すと次の部分です.
obtain ⟨x, _, hfg, hgf⟩ :
(univ ∩ { x | f x ≤ g x ∧ g x ≤ f x }).Nonempty :=
isPreconnected_closed_iff.1 PreconnectedSpace.isPreconnected_univ _ _
(isClosed_le hf hg) (isClosed_le hg hf)
(fun _ _ => le_total _ _) ⟨a, trivial, ha⟩ ⟨b, trivial, hb⟩
exact ⟨x, le_antisymm hfg hgf⟩
ここで考える閉集合は
です. 数式では
と置いています.
isClosed_le hf hg によって,連続関数の大小関係で定まる集合が閉集合であることが分かります.
また任意の点 x では le_total (f x) (g x) により,少なくともどちらか一方の閉集合に入ります.
つまり
です.
端点の仮定 f a ≤ g a と g b ≤ f b は,それぞれの閉集合が空でないことを与えます.
連結性はここで使われます.
isPreconnected_closed_iff は,preconnected な集合を 2 つの閉集合で覆い,
両方に点があるなら,2 つの閉集合の交わりにも点がある,という形の補題です.
数式では,
という使い方です.
その交点の点 x では
が同時に成り立つので,le_antisymm から f x = g x が出ます.
すなわち
次に,集合 s 上の定理
は,s を部分型として見て全空間版を適用します.
このとき Subtype.preconnectedSpace hs が「preconnected な集合 s は,
部分型として preconnected な空間になる」ことを与え,
continuousOn_iff_continuous_restrict が ContinuousOn f s を
部分型上の Continuous に読み替えます.
最後に,通常の中間値の定理
は,2 つ目の関数 g として定数関数 fun _ => c を取った特殊ケースです.
閉区間版
はさらに s = Icc a b とし,isPreconnected_Icc,
left_mem_Icc.2 hab,right_mem_Icc.2 hab を渡したものです.
実際の定理の本体はほぼ次の 1 行です.
つまり Mathlib の中間値の定理は,
- 連続関数の大小で定まる閉集合を作る.
- preconnected 性から,2 つの閉集合の交点を得る.
- 交点では両向きの不等式があるので等式にする.
- 一般の集合から閉区間へ特殊化する.
という流れで証明されています.
まとめ¶
Mathlib の位相では,極限を直接 ε-δ で定義するのではなく,フィルター Tendsto を基礎にします.
開集合・閉集合・閉包・内部は Set 上の述語や操作として扱われます.
距離空間では Metric.ball や dist を使い,連続性は Continuous,ContinuousAt,ContinuousOn で表します.
特に ContinuousAt f x は Tendsto f (𝓝 x) (𝓝 (f x)) なので,点での連続性もフィルターの極限として読めます.
中間値の定理では,閉区間 Icc a b が IsPreconnected であることと,
連続関数の大小関係で定まる閉集合が閉じていることを組み合わせます.
微分と積分の章では,これらの位相的な語彙がそのまま使われます.
形式化の tips¶
位相の形式化では,同じ定理が「集合の言葉」「フィルターの言葉」「距離の言葉」で出てきます. どのレベルで証明するかを選ぶのが重要です.
- 開集合・閉集合の問題なら
IsOpen,IsClosedを探す. - 極限の問題なら
Tendstoと𝓝を読む. - 距離空間の問題なら
Metric.ball,dist,Metric.mem_ballを使う. - 連続性の合成なら
Continuous.compまたはhg.comp hfを使う. - compactness では,Hausdorff 条件
[T2Space X]が必要かを確認する. - 中間値の定理では
intermediate_value_Iccとintermediate_value_Icc'を探す.
Chapter 04: 微分¶
Mathlib における基本的な微分を扱います.
参考:
- Mathematics in Lean, 12. Differential Calculus: https://leanprover-community.github.io/mathematics_in_lean/C12_Differential_Calculus.html
実数値関数の初等微分から,ノルム空間上の Fréchet 微分までが説明されています.
微分に関する主な述語・関数は次の通りです.
HasDerivAt f f' x: 1 変数関数fが点xで微分係数f'を持つ.DifferentiableAt ℝ f x:fが点xで微分可能である.deriv f x:fの点xにおける微分係数.微分不能な点では 0 と定義される.HasFDerivAt f f' x: ノルム空間上の Fréchet 微分.fderiv 𝕜 f x: Fréchet 微分としての導関数.
import Mathlib
namespace PracticeChapter04
noncomputable section
open Set Filter
open scoped Topology
Mathlib における微分の形式化¶
Mathlib の微分は,1 変数実関数の極限
から直接始めるのではなく,ノルム空間上の Fréchet 微分を中心に実装されています.
基礎になる型は,ノルム体 𝕜 上のノルム空間 E,F と,関数 f : E → F です.
点 x : E における微分は数ではなく,連続線形写像 f' : E →L[𝕜] F として表されます.
数学的には,HasFDerivAt f f' x は
という一次近似を表します.
Lean ではこの o はフィルター 𝓝 x に沿った漸近記法として表されます.
Mathlib の定義ファイルでは,さらに一般に HasFDerivAtFilter が用意されており,
通常の点での微分 HasFDerivAt,集合内での微分 HasFDerivWithinAt,逆関数定理などで使う strict derivative HasStrictFDerivAt が,
どのフィルターに沿って同じ一次近似を見るかの違いとして定義されています.
DifferentiableAt 𝕜 f x は「そのような連続線形写像 f' が存在する」という述語です.
一方 fderiv 𝕜 f x は導関数を値として返す関数です.
導関数が存在すればその値を返し,存在しなければ 0 に定義されます.
そのため,証明ではまず HasFDerivAt や HasDerivAt を作り,最後に .fderiv や .deriv で値としての導関数へ移ります.
1 変数の HasDerivAt f f' x は,この Fréchet 微分の特殊化です.
関数 f : 𝕜 → F の微分係数 f' : F を,h ↦ h • f' という連続線形写像に対応させて HasFDerivAt に戻しています.
特に f : ℝ → ℝ なら,いつもの微分係数は実数として現れます.
deriv f x は fderiv 𝕜 f x を方向 1 に評価したものです.
前半では HasDerivAt,deriv,Rolle の定理や平均値の定理を扱い,
後半ではノルム空間,連続線形写像 E →L[𝕜] F,漸近記法 =O・=o,HasFDerivAt,ContDiff,逆関数定理へ進みます.
漸近記法 =o を読む¶
Mathlib では,Landau の little-o 記法を
と書きます.
これは「フィルター l に沿って,f は g より十分小さい」という意味です.
たとえば l = 𝓝 x なら,変数が点 x に近づくときの漸近的な大小を表しています.
Mathlib のソースでは,次のように定義されています.
irreducible_def IsLittleO (l : Filter α) (f : α → E) (g : α → F) : Prop :=
∀ ⦃c : ℝ⦄, 0 < c → IsBigOWith c l f g
notation:100 f " =o[" l "] " g:100 => IsLittleO l f g
theorem isLittleO_iff :
f =o[l] g ↔ ∀ ⦃c : ℝ⦄, 0 < c → ∀ᶠ x in l, ‖f x‖ ≤ c * ‖g x‖
最後の isLittleO_iff が,数学的な読み方に一番近いです.
任意の正の定数 c に対して,l に沿って十分近いところでは
が成り立つ,という意味です.
つまり,f は g の任意に小さい定数倍で抑えられるほど小さい,ということです.
分母が 0 になる点を避けて直感的に書けば,‖f x‖ / ‖g x‖ → 0 です.
微分で出てくる
は,x' → x のとき,一次近似
からの誤差が,変位 x' - x に比べて高次の微小量であることを表しています.
これが「f' が点 x での微分である」という条件です.
=O は「ある定数倍で抑えられる」という有界な大小関係ですが,
=o は「任意に小さい定数倍で抑えられる」という,より強い条件です.
HasDerivAt と DifferentiableAt の定義を読む¶
1 変数の微分係数 HasDerivAt f f' x は,Mathlib のソースでは次のように定義されています.
def HasDerivAtFilter (f : 𝕜 → F) (f' : F) (L : Filter (𝕜 × 𝕜)) :=
HasFDerivAtFilter f (toSpanSingleton 𝕜 f') L
def HasDerivAt (f : 𝕜 → F) (f' : F) (x : 𝕜) :=
HasDerivAtFilter f f' (𝓝 x ×ˢ pure x)
ここで toSpanSingleton 𝕜 f' は,ベクトル f' : F を
h ↦ h • f' という連続線形写像 𝕜 →L[𝕜] F に変換するものです.
したがって数学的には,HasDerivAt f f' x は
という一次近似を表します.
特に f : ℝ → ℝ の場合,これは通常の
という微分係数の定義と対応します.
一方,DifferentiableAt は微分係数そのものを指定せず,「ある Fréchet 微分が存在する」と定義されています.
def HasFDerivAt (f : E → F) (f' : E →L[𝕜] F) (x : E) :=
HasFDerivAtFilter f f' (𝓝 x ×ˢ pure x)
def DifferentiableAt (f : E → F) (x : E) :=
∃ f' : E →L[𝕜] F, HasFDerivAt f f' x
つまり DifferentiableAt 𝕜 f x は,点 x の近くで
を満たす連続線形写像 f' : E →L[𝕜] F が存在する,という命題です.
HasDerivAt は微分係数の値まで指定する述語,DifferentiableAt は値を指定せず存在だけを主張する述語,と読むと使い分けやすいです.
#check HasFDerivAtFilter
#check HasFDerivAt
#check HasDerivAtFilter
#check HasDerivAt
#check fderiv
#check deriv
#check Asymptotics.IsLittleO
#check Asymptotics.isLittleO_iff
section DifferentialDefinitions
open Asymptotics
variable {𝕜 : Type*} [NontriviallyNormedField 𝕜]
variable {E F : Type*}
variable [NormedAddCommGroup E] [NormedSpace 𝕜 E]
variable [NormedAddCommGroup F] [NormedSpace 𝕜 F]
example {f : E → F} {f' : E →L[𝕜] F} {x : E} :
HasFDerivAt f f' x ↔
(fun x' => f x' - f x - f' (x' - x)) =o[𝓝 x] (fun x' => x' - x) := by
exact hasFDerivAt_iff_isLittleO
example {f : E → F} {f' : E →L[𝕜] F} {x : E} :
HasFDerivAt f f' x ↔
Filter.Tendsto
(fun x' => ‖x' - x‖⁻¹ * ‖f x' - f x - f' (x' - x)‖)
(𝓝 x)
(𝓝 0) := by
exact hasFDerivAt_iff_tendsto
example {f : 𝕜 → F} {f' : F} {x : 𝕜} :
HasDerivAt f f' x ↔
(fun x' => f x' - f x - (x' - x) • f') =o[𝓝 x] (fun x' => x' - x) := by
exact hasDerivAt_iff_isLittleO
example {f : 𝕜 → F} {x : 𝕜} : deriv f x = (fderiv 𝕜 f x) 1 := by
rfl
end DifferentialDefinitions
実数値関数の微分¶
実数から実数への関数では,点での微分係数を HasDerivAt で表します.
微分係数を明示しない場合は DifferentiableAt ℝ を使います.
#check HasDerivAt
#check DifferentiableAt
#check deriv
section RealDerivatives
open Real
example : HasDerivAt sin 1 0 := by
simpa using hasDerivAt_sin 0
example (x : ℝ) : DifferentiableAt ℝ sin x := by
exact (hasDerivAt_sin x).differentiableAt
example {f : ℝ → ℝ} {x a : ℝ} (h : HasDerivAt f a x) : deriv f x = a := by
exact h.deriv
example {f : ℝ → ℝ} {x : ℝ} (h : ¬ DifferentiableAt ℝ f x) : deriv f x = 0 := by
exact deriv_zero_of_not_differentiableAt h
example : deriv (fun x : ℝ => x ^ 5) 6 = 5 * 6 ^ 4 := by
calc
deriv (fun x : ℝ => x ^ 5) 6 = 5 * 6 ^ (5 - 1) := by
exact (hasDerivAt_pow 5 (6 : ℝ)).deriv
_ = 5 * 6 ^ 4 := by norm_num
example : deriv sin π = -1 := by
simp
end RealDerivatives
deriv f x は,任意の関数 f : ℝ → ℝ と点 x に対して定義されています.
ただし,微分可能でない点では値が 0 になります.
そのため,定理を使うときには HasDerivAt や DifferentiableAt の仮定が必要かを確認します.
微分の具体的な計算では,calc を使うと,紙の計算に近い形で式変形を読めます.
たとえば上の例では,まず deriv_pow で
を得て,次に norm_num で \(5-1=4\) を計算しています.
HasDerivAt や DifferentiableAt を作る部分は補題を使い,
導関数の係数や数値だけを calc で整理すると読みやすくなります.
演習問題¶
-
fun x : ℝ => x ^ 4が任意の点で微分可能であることを示してください. -
x^2の点3における導関数が6であることを示してください. -
HasDerivAtの証明からDifferentiableAtを取り出してください. -
derivは微分不能な点で 0 と定義されることを,絶対値関数などで調べてください.
微分の計算規則¶
和,積,合成などの微分公式は,HasDerivAt 版,DifferentiableAt 版,deriv 版など複数の形で用意されています.
まずは補題の型を #check で確認し,必要な仮定を揃えます.
#check HasDerivAt.add
#check HasDerivAt.mul
#check HasDerivAt.comp
#check deriv_add
#check deriv_mul
section DerivativeRules
example {f g : ℝ → ℝ} {x : ℝ}
(hf : DifferentiableAt ℝ f x) (hg : DifferentiableAt ℝ g x) :
deriv (fun y => f y + g y) x = deriv f x + deriv g x := by
exact deriv_add hf hg
example {f g : ℝ → ℝ} {x : ℝ}
(hf : DifferentiableAt ℝ f x) (hg : DifferentiableAt ℝ g x) :
deriv (fun y => f y * g y) x = deriv f x * g x + f x * deriv g x := by
exact deriv_mul hf hg
example {f g : ℝ → ℝ} {x a b : ℝ}
(hf : HasDerivAt f a x) (hg : HasDerivAt g b x) :
HasDerivAt (fun y => f y + g y) (a + b) x := by
exact hf.add hg
end DerivativeRules
演習問題¶
積の微分公式を使って,f * g の導関数を表してください.
example {f g : ℝ → ℝ} {x : ℝ}
(hf : DifferentiableAt ℝ f x) (hg : DifferentiableAt ℝ g x) :
deriv (fun y => f y * g y) x = deriv f x * g x + f x * deriv g x := by
-- ヒント: `exact deriv_mul hf hg`
-- 解答例: exact deriv_mul hf hg
sorry
Rolle の定理と平均値の定理¶
Mathlib には,実解析の基本定理も登録されています.
区間は Set.Icc a b,開区間は Set.Ioo a b として表します.
#check exists_deriv_eq_zero
#check exists_deriv_eq_slope
section MeanValue
open Set
example {f : ℝ → ℝ} {a b : ℝ}
(hab : a < b) (hfc : ContinuousOn f (Icc a b)) (hfI : f a = f b) :
∃ c ∈ Ioo a b, deriv f c = 0 := by
exact exists_deriv_eq_zero hab hfc hfI
example (f : ℝ → ℝ) {a b : ℝ}
(hab : a < b) (hf : ContinuousOn f (Icc a b))
(hf' : DifferentiableOn ℝ f (Ioo a b)) :
∃ c ∈ Ioo a b, deriv f c = (f b - f a) / (b - a) := by
exact exists_deriv_eq_slope f hab hf hf'
end MeanValue
演習問題¶
平均値の定理の statement を読み,ContinuousOn と DifferentiableOn の役割を説明してください.
ノルム空間¶
一般の微分では,実数直線だけでなくノルム空間を扱います.
NormedAddCommGroup E はノルムを持つ加法可換群,
NormedSpace ℝ E は実ノルムベクトル空間です.
#check NormedAddCommGroup
#check NormedSpace
#check norm_nonneg
#check norm_add_le
section NormedSpaces
variable {E : Type*} [NormedAddCommGroup E]
example (x : E) : 0 ≤ ‖x‖ := by
exact norm_nonneg x
example {x : E} : ‖x‖ = 0 ↔ x = 0 := by
exact norm_eq_zero
example (x y : E) : ‖x + y‖ ≤ ‖x‖ + ‖y‖ := by
exact norm_add_le x y
example : PseudoMetricSpace E := by
infer_instance
variable [NormedSpace ℝ E]
example (a : ℝ) (x : E) : ‖a • x‖ = |a| * ‖x‖ := by
exact norm_smul a x
example [FiniteDimensional ℝ E] : CompleteSpace E := by
infer_instance
end NormedSpaces
有限次元ノルム空間が完備であることなど,解析の標準的な背景定理も型クラス探索で得られることがあります.
infer_instance は,必要な型クラスインスタンスを Lean に探させるコマンドです.
連続線形写像と Frechet 微分¶
ノルム空間の間の連続線形写像は E →L[𝕜] F です.
Frechet 微分では,導関数の値は連続線形写像になります.
#check ContinuousLinearMap
#check HasFDerivAt
#check fderiv
section Frechet
variable {𝕜 : Type*} [NontriviallyNormedField 𝕜]
variable {E F : Type*}
variable [NormedAddCommGroup E] [NormedSpace 𝕜 E]
variable [NormedAddCommGroup F] [NormedSpace 𝕜 F]
example : E →L[𝕜] E :=
ContinuousLinearMap.id 𝕜 E
example (f : E →L[𝕜] F) : E → F :=
f
example (f : E →L[𝕜] F) : Continuous f := by
exact f.cont
example (f : E →L[𝕜] F) (x y : E) : f (x + y) = f x + f y := by
exact f.map_add x y
example (f : E →L[𝕜] F) (a : 𝕜) (x : E) : f (a • x) = a • f x := by
exact f.map_smul a x
example (f : E →L[𝕜] F) (x : E) : HasFDerivAt f f x := by
exact f.hasFDerivAt
end Frechet
HasFDerivAt f f' x は,「点 x での f の一次近似が連続線形写像 f' である」という意味です.
実数値関数の HasDerivAt は,この一般論の特殊な形として扱えます.
演習問題¶
-
E →L[ℝ] Fの元が連続関数として使えることを確認してください. -
HasFDerivAtの statement を#checkで表示し,実数 1 変数のHasDerivAtと何が違うか説明してください. -
ContinuousLinearMap.idが任意の点で Frechet 微分として自分自身を持つことを確認してください.
長めの例: 微分公式を組み合わせる¶
ここでは,関数
が x = 5 で微分係数 77 を持つことを示します.
紙の計算では f'(x) = 3x^2 + 2 なので f'(5) = 77 です.
Lean では,HasDerivAt の証明を組み合わせて同じことを表します.
section PolynomialDerivativeExample
theorem cubicExample_hasDerivAt :
HasDerivAt (fun x : ℝ => x ^ 3 + 2 * x + 1) (77 : ℝ) 5 := by
have hpow : HasDerivAt (fun x : ℝ => x ^ 3) (3 * 5 ^ (3 - 1)) 5 := by
simpa using (hasDerivAt_pow 3 (5 : ℝ))
have hlin : HasDerivAt (fun x : ℝ => 2 * x) (2 * 1) 5 := by
simpa using ((hasDerivAt_id (5 : ℝ)).const_mul (2 : ℝ))
have h := (hpow.add hlin).add_const (1 : ℝ)
have hcoeff : 3 * 5 ^ 2 + 2 = (77 : ℝ) := by
calc
(3 : ℝ) * 5 ^ 2 + 2 = 3 * 25 + 2 := by norm_num
_ = 77 := by norm_num
simpa [Pi.add_apply, hcoeff] using h
example : deriv (fun x : ℝ => x ^ 3 + 2 * x + 1) 5 = 77 := by
exact cubicExample_hasDerivAt.deriv
end PolynomialDerivativeExample
この例では,次の補題を使っています.
hasDerivAt_pow: 冪関数の微分hasDerivAt_id: 恒等関数の微分HasDerivAt.const_mul: 定数倍の微分HasDerivAt.add: 和の微分HasDerivAt.add_const: 定数を足しても微分係数は変わらない
最後は,Lean が持っている導関数の値を hcoeff で数値計算し,
simpa [Pi.add_apply, hcoeff] using h で関数の表示と導関数の値を整理しています.
hcoeff の中では calc を使い,
という数値計算を段階的に書いています.
convert を使うと,Mathlib のバージョンによっては型クラスインスタンスの等式まで
余分なゴールとして現れることがあるため,ここでは数値計算を明示的に分けています.
演習問題¶
x ↦ 3 * x ^ 2 の点 2 における微分係数を HasDerivAt で示してください.
example : HasDerivAt (fun x : ℝ => 3 * x ^ 2) (12 : ℝ) 2 := by
-- `hasDerivAt_pow` と `.const_mul` を使う.
-- 解答例:
-- have hpow : HasDerivAt (fun x : ℝ => x ^ 2) (2 * 2 ^ (2 - 1)) 2 := by
-- simpa using (hasDerivAt_pow 2 (2 : ℝ))
-- have h := hpow.const_mul (3 : ℝ)
-- have hcoeff : (3 : ℝ) * (2 * 2) = 12 := by
-- calc
-- (3 : ℝ) * (2 * 2) = 3 * 4 := by norm_num
-- _ = 12 := by norm_num
-- simpa [mul_assoc, hcoeff] using h
sorry
長めの例: 閉区間上の Rolle の定理を使う¶
次の例は,Rolle の定理を直接使う演習です.
関数 f が閉区間 [a, b] で連続で,端点で同じ値を取るなら,
開区間 (a, b) のどこかで導関数が 0 になります.
この statement はこの章の前半にも出しましたが,ここでは「学部解析の定理をそのまま Lean の命題として読む」ことに注目します.
section RolleExample
open Set
example {f : ℝ → ℝ} {a b : ℝ}
(hab : a < b)
(hcont : ContinuousOn f (Icc a b))
(hend : f a = f b) :
∃ c ∈ Ioo a b, deriv f c = 0 := by
exact exists_deriv_eq_zero hab hcont hend
end RolleExample
この定理を使うには,ContinuousOn f (Icc a b) が必要です.
微分可能性の仮定が statement に現れていないように見えますが,
Mathlib のこの定理は,一般の Rolle の定理のうち,導関数が定義上 0 になる点を含む形になっています.
より強い形や平均値の定理を使うときは,DifferentiableOn ℝ f (Ioo a b) が明示的に必要になります.
まとめ¶
微分では,まず HasDerivAt,DifferentiableAt,deriv の違いを押さえることが重要です.
初等的な計算は simp や既存の微分公式で進みます.
より一般の解析では,ノルム空間,連続線形写像,Frechet 微分 HasFDerivAt が基本語彙になります.
形式化の tips¶
微分の形式化では,deriv の値を直接計算するより,まず HasDerivAt の証明を作る方が安定することがあります.
- 点での微分係数を主張するなら
HasDerivAt. - 微分可能性だけなら
DifferentiableAt. - 導関数の値を式として使うなら
deriv. - 多変数・ノルム空間なら
HasFDerivAt. - 計算が詰まったら,
#check HasDerivAt.addのように公式を探す.
Chapter 05: 積分¶
Mathlib における基本的な測度と積分を扱います.
- Mathematics in Lean, 13. Integration and Measure Theory: https://leanprover-community.github.io/mathematics_in_lean/C13_Integration_and_Measure_Theory.html
区間積分,測度論,Bochner 積分,優収束定理,Fubini の定理などが概観されています.
Mathlib の積分はかなり一般的です.
実数値関数だけでなく,Banach 空間値の Bochner 積分を基本としており,測度は Measure α,値は拡張非負実数 ℝ≥0∞ を使います.
import Mathlib
namespace PracticeChapter05
noncomputable section
open Set Filter MeasureTheory
open scoped BigOperators ENNReal Topology Interval
測度空間と可測集合¶
可測空間構造は MeasurableSpace α です.
測度は Measure α で,可測集合だけでなく任意の集合 s : Set α に対して μ s : ℝ≥0∞ が定義されています.
ただし,多くの定理では可測性の仮定が必要です.
#check MeasurableSpace
#check Measure
#check MeasurableSet
#check (fun {α : Type*} [MeasurableSpace α] => Measure α)
section MeasurableSets
variable {α : Type*} [MeasurableSpace α]
example : MeasurableSet (∅ : Set α) := by
exact MeasurableSet.empty
example : MeasurableSet (univ : Set α) := by
exact MeasurableSet.univ
example {s : Set α} (hs : MeasurableSet s) : MeasurableSet sᶜ := by
exact hs.compl
variable {ι : Type*} [Encodable ι]
example {f : ι → Set α} (h : ∀ i, MeasurableSet (f i)) :
MeasurableSet (⋃ i, f i) := by
exact MeasurableSet.iUnion h
example {f : ι → Set α} (h : ∀ i, MeasurableSet (f i)) :
MeasurableSet (⋂ i, f i) := by
exact MeasurableSet.iInter h
end MeasurableSets
可算和や可算共通部分には,添字型が可算であることが必要です.
上の例では [Encodable ι] によって,ι が可算に符号化できることを仮定しています.
演習問題¶
-
可測集合の補集合が可測であることを示してください.
-
MeasurableSet.iUnionの仮定を読み,可算性がどこで必要か確認してください.
測度¶
測度 μ : Measure α は集合に ℝ≥0∞ の値を割り当てます.
ℝ≥0∞ は拡張非負実数で,無限大 ∞ を含みます.
#check ENNReal
#check (∞ : ℝ≥0∞)
#check MeasureTheory.Measure
#check measure_iUnion_le
section Measures
variable {α : Type*} [MeasurableSpace α]
variable {μ : Measure α}
example (s : Set α) : μ s = ⨅ (t : Set α) (_ : s ⊆ t) (_ : MeasurableSet t), μ t := by
exact measure_eq_iInf s
example {ι : Type*} [Encodable ι] (s : ι → Set α) :
μ (⋃ i, s i) ≤ ∑' i, μ (s i) := by
exact measure_iUnion_le s
example {P : α → Prop} : (∀ᵐ x ∂μ, P x) ↔ ∀ᶠ x in ae μ, P x := by
rfl
end Measures
∀ᵐ x ∂μ, P x は「μ に関してほとんど至る所 P x が成り立つ」という意味です.
これはフィルター ae μ による Eventually の記法です.
演習問題¶
ほとんど至る所の記法 ∀ᵐ が filter の Eventually であることを確認してください.
example {α : Type*} [MeasurableSpace α] {μ : Measure α} {P : α → Prop} :
(∀ᵐ x ∂μ, P x) ↔ ∀ᶠ x in ae μ, P x := by
-- ヒント: `rfl`
sorry
Bochner 積分¶
Mathlib の標準的な積分 ∫ x, f x ∂μ は Bochner 積分です.
値域は実数だけでなく,完備なノルム空間に一般化されています.
多くの定理では Integrable f μ という仮定を使います.
#check Integrable
#check integral_add
#check setIntegral_const
section BochnerIntegral
variable {α : Type*} [MeasurableSpace α]
variable {μ : Measure α}
variable {E : Type*} [NormedAddCommGroup E] [NormedSpace ℝ E] [CompleteSpace E]
example {f g : α → E} (hf : Integrable f μ) (hg : Integrable g μ) :
∫ a, f a + g a ∂μ = ∫ a, f a ∂μ + ∫ a, g a ∂μ := by
exact integral_add hf hg
example {s : Set α} (c : E) : ∫ _ in s, c ∂μ = (μ s).toReal • c := by
exact setIntegral_const c
end BochnerIntegral
定数関数の積分では,測度値 μ s : ℝ≥0∞ を実数に戻すために (μ s).toReal が現れます.
μ s = ∞ の場合には,非零定数関数は可積分でなく,積分は定義上 0 になるという規約とも整合しています.
演習問題¶
Bochner 積分の加法性を使ってください.
example {α E : Type*} [MeasurableSpace α]
[NormedAddCommGroup E] [NormedSpace ℝ E] [CompleteSpace E]
{μ : Measure α} {f g : α → E}
(hf : Integrable f μ) (hg : Integrable g μ) :
∫ x, f x + g x ∂μ = ∫ x, f x ∂μ + ∫ x, g x ∂μ := by
-- ヒント: `exact integral_add hf hg`
sorry
区間積分¶
実数直線上の区間積分は ∫ x in a..b, f x と書きます.
これは向きつきの区間積分で,a から b へ積分します.
#check intervalIntegral.integral_of_le
#check intervalIntegral.integral_hasStrictDerivAt_right
section IntervalIntegral
example : ∫ _ : ℝ in (0)..(1), (1 : ℝ) = 1 := by
norm_num
example (f : ℝ → ℝ) (hf : Continuous f) (a b : ℝ) :
deriv (fun u => ∫ x : ℝ in a..u, f x) b = f b := by
exact (intervalIntegral.integral_hasStrictDerivAt_right
(hf.intervalIntegrable _ _)
(hf.stronglyMeasurableAtFilter _ _)
hf.continuousAt).hasDerivAt.deriv
end IntervalIntegral
上の例は微分積分学の基本定理の一方向です.
積分を上端 u の関数と見たとき,その導関数が被積分関数 f になることを述べています.
収束定理と Fubini の定理¶
測度論の大きな定理も Mathlib に登録されています. 最初は全文を展開するより,定理名と型を確認して使うのが現実的です.
#check tendsto_integral_of_dominated_convergence
#check integral_prod
#check integral_image_eq_integral_abs_det_fderiv_smul
section Fubini
variable {α β : Type*}
variable [MeasurableSpace α] [MeasurableSpace β]
variable {μ : Measure α} {ν : Measure β}
variable [SigmaFinite μ] [SigmaFinite ν]
variable {E : Type*} [NormedAddCommGroup E] [NormedSpace ℝ E] [CompleteSpace E]
example (f : α × β → E) (hf : Integrable f (μ.prod ν)) :
∫ z, f z ∂ μ.prod ν = ∫ x, ∫ y, f (x, y) ∂ν ∂μ := by
exact integral_prod f hf
end Fubini
演習問題¶
-
Fubini の定理の statement を
#checkで読み,どこにSigmaFinite仮定が現れるか確認してください. -
優収束定理の statement を読み,どの仮定が「支配関数」に対応するか確認してください.
長めの例: 区間積分の具体計算¶
区間積分は,初等解析の例では norm_num や ring_nf と連携して計算できることがあります.
まずは定数関数と恒等関数の積分を見ます.
section ConcreteIntervalIntegrals
example : ∫ _ : ℝ in (0)..(2), (3 : ℝ) = 6 := by
norm_num
example : ∫ x : ℝ in (0)..(1), x = (1 / 2 : ℝ) := by
norm_num
example : ∫ x : ℝ in (0)..(2), x = (2 : ℝ) := by
norm_num
end ConcreteIntervalIntegrals
これらの例は,内部では区間積分に関する既存定理と実数計算を使っています.
複雑な被積分関数では norm_num だけで閉じないことも多く,
その場合は微分積分学の基本定理や積分の線形性を明示的に使います.
演習問題¶
-
定数関数の区間積分を計算してください.
-
区間の向きを反転したときの積分を調べてください.
-
∫ x in a..b, f xと∫ x in b..a, f xの関係を使って,定数関数の例を逆向き区間で計算してください.
長めの例: 区間積分の線形性¶
積分の線形性は,関数の可積分性を仮定して使います.
区間積分では IntervalIntegrable f volume a b がよく現れます.
section LinearityOfIntervalIntegral
example {f g : ℝ → ℝ} {a b : ℝ}
(hf : IntervalIntegrable f volume a b)
(hg : IntervalIntegrable g volume a b) :
∫ x : ℝ in a..b, f x + g x =
(∫ x : ℝ in a..b, f x) + ∫ x : ℝ in a..b, g x := by
exact intervalIntegral.integral_add hf hg
example (c : ℝ) (f : ℝ → ℝ) (a b : ℝ) :
∫ x : ℝ in a..b, c * f x = c * ∫ x : ℝ in a..b, f x := by
exact intervalIntegral.integral_const_mul c f
end LinearityOfIntervalIntegral
紙の数学では,積分の線形性はほとんど自明に使います. Lean では,被積分関数が適切に積分可能であることが theorem の仮定に現れます. 定数倍については theorem が仮定なしの形で使えることもありますが,これは積分が定義上全関数に拡張されているためです. 本格的な解析では,可積分性の仮定を明示して使うのが安全です.
演習問題¶
積分の和に関する線形性を intervalIntegral.integral_add で証明してください.
example {f g : ℝ → ℝ} {a b : ℝ}
(hf : IntervalIntegrable f volume a b)
(hg : IntervalIntegrable g volume a b) :
∫ x : ℝ in a..b, f x + g x =
(∫ x : ℝ in a..b, f x) + ∫ x : ℝ in a..b, g x := by
-- ヒント: `exact intervalIntegral.integral_add hf hg`
sorry
まとめ¶
積分の章では,MeasurableSpace,Measure,MeasurableSet,Integrable,∫ 記法,∀ᵐ 記法を読むことが第一歩です.
実数値積分だけを見ている場合でも,Mathlib の内部では Bochner 積分と測度論の一般的な枠組みが使われます.
したがって,積分の証明では,可測性,可積分性,完備性,σ有限性などの仮定がどこで必要になるかを #check で確認しながら進めるのが重要です.
形式化の tips¶
積分の形式化では,計算そのものよりも仮定の整理が難しいことが多いです.
- 可測集合かどうかは
MeasurableSet. - 関数の可測性は
MeasurableやAEStronglyMeasurable. - 積分可能性は
Integrable. - ほとんど至る所は
∀ᵐ x ∂μ, .... - 区間積分では
IntervalIntegrable f volume a b. - 直積測度や Fubini では
SigmaFiniteまたはSFinite仮定を確認する.
Chapter 06: 確率論¶
Mathlib における基本的な確率論を概観します.
参考:
- Basic probability in Mathlib: https://leanprover-community.github.io/blog/posts/basic-probability-in-mathlib/
- Rémy Degenne, Markov kernels in Mathlib's probability library: https://arxiv.org/abs/2510.04070
前章の測度論・積分の上に,確率測度,事象,確率変数,期待値,独立性,条件付き確率,Markov kernel が定義されています.
確率論の形式化では,紙の数学で「確率空間」と一言で済ませる部分を,Lean ではいくつかの型と型クラスに分けて書きます.
典型的には,標本空間は型 Ω,可測構造は [MeasurableSpace Ω],確率測度は P : Measure Ω と [IsProbabilityMeasure P] です.
import Mathlib
namespace PracticeChapter06
noncomputable section
open Set Filter MeasureTheory ProbabilityTheory
open scoped BigOperators ENNReal ProbabilityTheory
確率空間と確率測度¶
Mathlib では,測度論の Measure Ω をそのまま使い,確率測度であることを型クラス IsProbabilityMeasure P で表します.
定義上,IsProbabilityMeasure P は P univ = 1 を主張する命題です.
ここで P s の値は ℝ≥0∞,つまり拡張非負実数です.
確率測度の場合は全体の測度が 1 なので,各事象の確率は ∞ にはなりません.
#check Measure
#check IsProbabilityMeasure
#check ProbabilityMeasure
#check measure_univ
#check measure_ne_top
#check measure_lt_top
section ProbabilitySpaces
variable {Ω : Type*} [MeasurableSpace Ω]
variable {P : Measure Ω} [IsProbabilityMeasure P]
example : P univ = 1 := by
simp
example (s : Set Ω) : P s ≤ 1 := by
exact prob_le_one
example (s : Set Ω) : P s ≠ ∞ := by
simp
example (s : Set Ω) : P s < ∞ := by
simp
example (s : Set Ω) : ℝ≥0∞ :=
P s
end ProbabilitySpaces
ProbabilityMeasure Ω という型もあります.
これは確率測度を subtype として束ねた型で,確率測度全体の空間や弱収束などを扱うときに使われます.
普通の定理を書くときは,P : Measure Ω と [IsProbabilityMeasure P] を仮定する形の方が,既存の測度論ライブラリと合わせやすいです.
標本空間に標準の測度を持たせたい場合は [MeasureSpace Ω] を使います.
この標準測度は volume で,確率論のスコープを開くと ℙ と書けます.
ただし,ℙ が確率測度であることは別に [IsProbabilityMeasure (ℙ : Measure Ω)] と仮定します.
section CanonicalMeasure
variable {Ω : Type*} [MeasureSpace Ω] [IsProbabilityMeasure (ℙ : Measure Ω)]
example : (ℙ : Measure Ω) univ = 1 := by
simp
example (s : Set Ω) : (ℙ : Measure Ω) s ≤ 1 := by
exact prob_le_one
end CanonicalMeasure
演習問題¶
確率測度では全体の測度が 1 であることを確認してください.
example {Ω : Type*} [MeasurableSpace Ω] {P : Measure Ω} [IsProbabilityMeasure P] :
P univ = 1 := by
sorry
事象と条件付き確率¶
Mathlib に「事象」という専用の型はありません.
事象は s : Set Ω として表し,多くの定理では MeasurableSet s を仮定します.
確率は単に測度の適用 P s です.
条件付き確率は P[s | t] と書けます.
これは条件付き測度 P[|t] を事象 s に適用したものです.
定義を展開すると,条件にしている集合 t が可測であるとき,
P[s | t] = (P t)⁻¹ * P (t ∩ s) になります.
分母が 0 の場合も,ℝ≥0∞ の逆元を使った全域的な定義として扱われます.
section Events
variable {Ω : Type*} [MeasurableSpace Ω]
variable {P : Measure Ω}
variable {s t : Set Ω}
#check MeasurableSet
#check ProbabilityTheory.cond
#check (P[|t])
#check (P[s | t])
example : P[s | t] = P[|t] s := by
rfl
example (ht : MeasurableSet t) : P[s | t] = (P t)⁻¹ * P (t ∩ s) := by
rw [cond_apply ht]
end Events
演習問題¶
条件付き確率の定義を展開してください.
example {Ω : Type*} [MeasurableSpace Ω] {P : Measure Ω}
{s t : Set Ω} (ht : MeasurableSet t) :
P[s | t] = (P t)⁻¹ * P (t ∩ s) := by
sorry
確率変数,分布,期待値¶
確率変数は,測度空間から可測空間への可測関数です.
Lean では関数 X : Ω → E と可測性の仮定 hX : Measurable X を分けて持ちます.
確率変数 X の分布は,測度の push-forward P.map X です.
期待値は積分で,∫ ω, X ω ∂P と書けます.
確率論スコープでは P[X] という記法も使えます.
section RandomVariables
variable {Ω E : Type*}
variable [MeasurableSpace Ω] [MeasurableSpace E]
variable [NormedAddCommGroup E] [NormedSpace ℝ E] [CompleteSpace E]
variable {P : Measure Ω} [IsProbabilityMeasure P]
variable {X Y : Ω → E}
#check Measurable
#check Measure.map
#check (P.map X)
#check (fun X : Ω → ℝ => P[X])
example {X : Ω → E} (hX : AEMeasurable X P) : IsProbabilityMeasure (P.map X) := by
exact Measure.isProbabilityMeasure_map hX
example (c : E) : ∫ _ : Ω, c ∂P = c := by
simp
example {X Y : Ω → E} (hX : Integrable X P) (hY : Integrable Y P) :
∫ ω, X ω + Y ω ∂P = ∫ ω, X ω ∂P + ∫ ω, Y ω ∂P := by
exact integral_add hX hY
end RandomVariables
P[X] は Bochner 積分の記法です.
したがって値域 E にはノルム空間の構造が必要です.
ℝ≥0∞ 値の非負関数を積分するときは,前章で見た Lebesgue 積分 ∫⁻ を使います.
この点は紙の確率論では同じ「期待値」と呼ばれがちですが,Mathlib では型によって使う積分が分かれます.
演習問題¶
定数確率変数の期待値を計算してください.
example {Ω : Type*} [MeasurableSpace Ω] {P : Measure Ω} [IsProbabilityMeasure P]
(c : ℝ) : ∫ _ : Ω, c ∂P = c := by
sorry
離散確率と PMF¶
離散確率では,可測性が問題にならないことが多いです.
Mathlib では [DiscreteMeasurableSpace Ω] が「すべての集合が可測である」ことを表します.
その場合,任意の関数 X : Ω → E は可測関数になります.
確率質量関数は PMF Ω で表されます.
p : PMF Ω から測度 p.toMeasure : Measure Ω を作ることができ,これは確率測度です.
有限集合上の一様分布や Bernoulli 分布も PMF として用意されています.
section DiscreteProbability
variable {Ω E : Type*}
variable [MeasurableSpace Ω] [DiscreteMeasurableSpace Ω]
variable [MeasurableSpace E]
#check DiscreteMeasurableSpace
#check MeasurableSet.of_discrete
#check Measurable.of_discrete
#check PMF
#check PMF.toMeasure
#check PMF.uniformOfFintype
#check PMF.bernoulli
example (s : Set Ω) : MeasurableSet s := by
exact MeasurableSet.of_discrete
example (X : Ω → E) : Measurable X := by
exact Measurable.of_discrete
example (p : PMF Ω) : IsProbabilityMeasure p.toMeasure := by
infer_instance
example (p : PMF Ω) : p.toMeasure univ = 1 := by
simp
end DiscreteProbability
演習問題¶
-
離散可測空間では任意の集合が可測であることを確認してください.
-
PMFから作った測度が確率測度であることを確認してください.
独立性と同分布¶
独立性も,関数・集合・可測空間に対してそれぞれ定義されています.
確率変数 X : Ω → E と Y : Ω → F の独立性は IndepFun X Y P と書きます.
集合の独立性は IndepSet s t P,族の独立性は iIndepFun や iIndepSet です.
同分布は IdentDistrib X Y P Q で表します.
まずは定義を展開して使うより,定理の仮定に現れる型を読めることが重要です.
section Independence
variable {Ω E F : Type*}
variable [MeasurableSpace Ω] [MeasurableSpace E] [MeasurableSpace F]
variable {P : Measure Ω}
variable {X : Ω → E} {Y : Ω → F}
#check IndepFun
#check iIndepFun
#check IndepSet
#check iIndepSet
#check IdentDistrib
example (h : IndepFun X Y P) : IndepFun Y X P := by
exact h.symm
end Independence
演習問題¶
独立性の対称性を使ってください.
example {Ω E F : Type*} [MeasurableSpace Ω] [MeasurableSpace E] [MeasurableSpace F]
{P : Measure Ω} {X : Ω → E} {Y : Ω → F}
(h : IndepFun X Y P) : IndepFun Y X P := by
sorry
Markov kernel¶
Markov kernel は,状態 a : α ごとに測度 κ a : Measure β を返す可測な写像です.
Mathlib では Kernel α β が kernel を表し,その各値が確率測度であることを IsMarkovKernel κ で表します.
参照論文では,Mathlib の確率論ライブラリが Markov kernel を広く使っていることが説明されています. 条件付き分布,posterior distribution,独立性・条件付き独立性の統一的な定義,sub-Gaussian random variables,entropy,Kullback-Leibler divergence などで kernel が中心的な役割を持ちます. この章では入口だけを見ます.
section MarkovKernels
variable {α β : Type*}
variable [MeasurableSpace α] [MeasurableSpace β]
#check Kernel
#check IsMarkovKernel
#check IsFiniteKernel
#check IsSFiniteKernel
#check Kernel.measurable
#check Kernel.bound
#check Kernel.deterministic
#check condDistrib
example (κ : Kernel α β) : Measurable κ := by
exact κ.measurable
example (κ : Kernel α β) [IsMarkovKernel κ] (a : α) : κ a univ = 1 := by
simp
example : IsFiniteKernel (0 : Kernel α β) := by
infer_instance
variable {f : α → β} (hf : Measurable f)
example : IsMarkovKernel (Kernel.deterministic f hf) := by
infer_instance
example (a : α) : Kernel.deterministic f hf a = Measure.dirac (f a) := by
exact Kernel.deterministic_apply hf a
end MarkovKernels
演習問題¶
deterministic kernel が Dirac 測度を返すことを確認してください.
example {α β : Type*} [MeasurableSpace α] [MeasurableSpace β]
{f : α → β} (hf : Measurable f) (a : α) :
Kernel.deterministic f hf a = Measure.dirac (f a) := by
sorry
まとめ¶
Mathlib の確率論は,測度論の上に構築されています.
確率空間は Ω,MeasurableSpace Ω,P : Measure Ω,IsProbabilityMeasure P に分けて読みます.
事象は Set Ω,確率変数は可測関数,分布は P.map X,期待値は積分です.
離散確率では PMF が便利ですが,定理としては Measure と DiscreteMeasurableSpace の形で書く方が既存ライブラリと接続しやすいことがあります.
条件付き確率や条件付き分布に進むと,Markov kernel が自然に現れます.
確率論の Mathlib コードを読むときは,確率論固有の用語と測度論の語彙がどの型で対応しているかを確認してください.