kururu_goedel’s diary

アメリカ在住中年男性集合論者のブログ

attributeとかその他の公理いくつかとか

attribute

前回書いた通り、例えば variable {V : Type*} [SetStructure V] [hV_Nonempty: Nonempty V] [hV_ZFC : V ⊧ₘ* 𝗭𝗙𝗖]とかしてやると、VはZFCのモデルとして振る舞うことになる。それはとても良いのだけれども、Z.leanはV ⊧ₘ* 𝗭という仮定のもとに書かれていて、このV ⊧ₘ* 𝗭𝗙𝗖という仮定だと自動的には適用されない。みたい。例えば、これは[V ⊧ₘ* 𝗭]を消すと通らない。それどころか、∅がこの場合なにかわからないと言い出す。

example [V ⊧ₘ* 𝗭] : IsEmpty (∅ : V) := by exact IsEmpty.empty

いちいちそういうの書くの嫌だなぁと思っていたら、こうすれば大丈夫らしい。

lemma hV_ZF : V ⊧ₘ* 𝗭𝗙 := by
  unfold ZermeloFraenkelChoice at hV_ZFC
  exact ModelsTheory.of_add_left V 𝗭𝗙 𝗔𝗖

lemma hV_Z : V ⊧ₘ* 𝗭 := by
  apply ModelsTheory.of_ss hV_ZF z_subset_zf

-- By doing this, hV_Z and hV_ZF can be used without explicitly passing them.
attribute [local instance] hV_Z
attribute [local instance] hV_ZF

-- Now, it works without [V ⊧ₘ* 𝗭].
example : IsEmpty (∅ : V) := by exact IsEmpty.empty

つまり、V ⊧ₘ* 𝗭𝗙𝗖という仮定から、V ⊧ₘ* 𝗭V ⊧ₘ* 𝗭𝗙も証明できるのでやっておいて、それをattributeというコマンドを使って登録しておくと、自動的に見つけて仮定として使ってくれるらしい。これはほぼ常にZFで作業をする私にとってはとても便利である。なお、localと断っているので、sectionが終わったらそこで効力は終わる。

ext tactic

Finsetのときも外延性の公理を用いて集合が等しいことを示すextというtacticを使ったが、これはZのモデルでも使えるようにZ.leanで設定されている。というわけで、定番の空集合はユニークっていうのはこう書ける。

example : ∀ x : V, (∀ y : V, y ∉ x) ↔ x = (∅ : V):= by
  intro x
  constructor
  case mp =>
    intro h
    ext z
    constructor
    case a.mp =>
      intro hz
      absurd hz
      apply h
    case a.mpr =>
      intro hz
      simp only [not_mem_empty] at hz
  case mpr =>
    intro hx
    rw [hx]
    simp only [not_mem_empty, not_false_eq_true, implies_true]

実はすでにZ.leanでisEmpty_iff_eq_emptyという定理として示されているんだけど。まあ、車輪の再発明上等である。

2つの集合の和集合

2つの集合xとyの和集合を表すx ∪ yも定義されているので、トリプルトンもそうやって定義することができる。まあ、結局はsUnionを使った場合とあまり変わらないのだが。

-- Rewrite by using union of two sets.
example : ∀ a b c : V, ∃ x : V, ∀ y : V, y ∈ x ↔ y = a ∨ y = b ∨ y = c := by
  intro a b c
  use (doubleton a b) ∪ (singleton c)
  intro y
  constructor
  case h.mp =>
    intro h
    simp only [mem_union_iff, mem_doubleton_iff] at h
    obtain (ha | hb) | hc := h
    case inl.inl =>
      left; exact ha
    case inl.inr =>
      right; left; exact hb
    case inr =>
      apply mem_singleton_iff.mp at hc
      right; right; exact hc
  case h.mpr =>
    intro h
    simp only [mem_union_iff, mem_doubleton_iff]
    obtain ha | hb | hc := h
    case inl =>
      left; left; exact ha
    case inr.inl =>
      left; right; exact hb
    case inr.inr =>
      right
      apply mem_singleton_iff.mpr hc

有限集合の定義

いや、まあ、こんなことをやらなくてもトリプルトンは{a, b, c}とかで書けてしまうんだけど。

example : ∀ a b c : V, ∃ x : V, ∀ y : V, y ∈ x ↔ y = a ∨ y = b ∨ y = c := by
  intro a b c
  use {a, b, c}
  intro y
  simp only [mem_insert, mem_singleton_iff]

こういうのを実装するのにどうしたらいいのかは皆目見当がつかないので、よくわかっている人が整備してくれるのは本当に助かる。

順序対

xとyの順序対、{{x}, {x, y}}はKuratowski Pairということでkpair x yで記述できる。記号では⟨x, y⟩ₖ。この小さいkを出すには"_k"ね。 それ以上のタプルにも対応しているようで、例えば⟨x, y, z⟩ₖは⟨x, ⟨y, z⟩ₖ⟩ₖとして実装されているようだ。というわけで、example (x1 y1 z1 x2 y2 z2 : V) : x1 = x2 ∧ y1 = y2 ∧ z1 = z2 ↔ ⟨x1, y1, z1⟩ₖ =⟨x2, y2, z2⟩ₖとかも証明できる。

べき集合

xのべき集合はpower xで書ける。練習にlemma power_singleton_empty : power ({∅} : V) = {∅, {∅}}とか証明したりしてみる。

無限の公理

ωはωで書けるようになっている。なので、(∅ : V) ∈ (ω : V)とか∀ x : V, (x ∈ (ω : V) → x ∪ {x} ∈ (ω : V))とか証明できるというかすでにできてる。いや、後者はいくつか定義をほどかないといけないので少しやることはある。もちろんこれらだけではユニークにならないので、そういうののなかで最小のものと書いてある。

次回予告

さて、次はいよいよ分出公理ということになる。これはこれまでと違って、図式になっているだけじゃなくて、「V上で定義可能」という概念を扱わないといけないことになる。論理式で書いちゃえばそれでおしまいといえばそうなのだけれども、思想として極力モデルで解釈して書きたいと思っているわけで、そこの架け橋がどう実装されてそれをどう運用するかというのは重要な問題である。ってわけで、ぼちぼちやっていきます。

FormalizedFormalLogic事始め

SnO₂WMaNさんとpalalansoukîさんがやっているFormalized Formal Logic · GitHub というとても興味深いプロジェクトがありまして、去年ZFCを形式化しようとしていたのが目に止まったらしく、ZF集合論関係のことをできないかと言われたあと仕事+プライベートで色々あって完全放置になっていたのですが、ちょっとずつやっていこうと思います。

公理系

公理系の記述はFoundation.FirstOrder.SetTheory.Basic.Axiom.leanにある。Sentence ℒₛₑₜとかいうのは言語がℒₛₑₜな文の型なんだろうなぁとか推測しながら見ていれば素直にわかる。なお、そのあたりの型はFoundation.FirstOrder.Basic.Syntax.Formula.leanで定義されている。例えば

def empty : Sentence ℒₛₑₜ := “∃ e, ∀ y, y ∉ e”

ただ、スキーマになる分出公理と置換公理はSyntacticSemiformulaという型の引数を取っているが、まあそれもなんとなくは分かる。

それらの個別の公理をまとめて、Theory ℒₛₑₜという型のZermelo, ZermeloFraenkel, ZermeloFraenkelChoiceなどというのが定義されている。

モデル

それらのモデルを作るには、variable {V : Type*} [SetStructure V] [Nonempty V] [V ⊧ₘ* 𝗭𝗙𝗖]などと書けば良い。らしい。⊧ₘは左辺が右辺の理論のモデルになっていることを意味する。がついているのは右側が理論、すなわち文の集合になっていることを指し、*がついていない⊧ₘは右辺は文になる。

空集合

ここから何をやるにも、まず、VがZFCのそれぞれの公理を満たすことがわかってないといけない。例えばV ⊧ₘ Axiom.emptyとかが言えないと先に進まない。この時点で戸惑っていたのだけれども、最終的に書けたのがこれ。

lemma V_models_empty : V ⊧ₘ Axiom.empty := by
  -- You can split V ⊧ₘ* 𝗭𝗙𝗖 into V ⊧ₘ* 𝗭𝗙 ∧ V ⊧ₘ* 𝗔𝗖
  simp only [ModelsTheory.add_iff] at hV_ZFC
  obtain ⟨hV_ZF, hV_AC⟩ := hV_ZFC
  -- By this, we can interpret ⊧ₘ* by using ⊧ₘ
  apply modelsTheory_iff.mp at hV_ZF
  -- Use ZermeloFraenkel.axiom_of_empty_set, the instance of Axiom.empty
  apply hV_ZF ZermeloFraenkel.axiom_of_empty_set

𝗭𝗙𝗖は𝗭𝗙 + 𝗔𝗖として定義されているので、それを使って分解して、⊧ₘ* は右辺に属する文を全部満たすことだよとmodelsTheory_iff.mpfで解釈してから、Axiom.emptyが𝗭𝗙に属していることを使う。 というところで、それより更に進んだものがすでにZ.leanでやられていることを知る。

lemma empty_exists : ∃ e : V, IsEmpty e := by simpa [models_iff] using ModelsTheory.models V Zermelo.axiom_of_empty_set

def IsEmpty (a : V) : Prop := ∀ x, x ∉ aとなっているので、これはまさにAxiom.emptyを解釈したあとの姿になっている。 えーと、こんなもの読んでいる人はさすがに数理論理学の基礎の基礎は知っている人たちだろうから、説明は適当でいいよね。つまり、Vという構造が“∃ e, ∀ y, y ∉ e”という文を満たすということの定義が、この文の中の∈をV上で定義されている∈であると解釈するならば、∃ e : V, ∀ x, x ∉ aになるということ。モデルの充足の話を外の話に持ってきたわけだ。 去年はこういうのを自動化したいといろいろがんばっていたわけで、形式証明ツヨツヨ勢はすぐにこんなふうにできちゃうんだなぁと感服したわけです。

さらに、空集合となるe : Vが存在するのだから、それを定数として確保したくなる。これもZ.leanに気づく前に自前で書いてみたのだけれども、Z.leanではこのように書いてある。

noncomputable scoped instance : EmptyCollection V := ⟨Classical.choose! empty_existsUnique⟩

@[simp] lemma IsEmpty.empty : IsEmpty (∅ : V) := Classical.choose!_spec empty_existsUnique

empty_existsUniqueは空集合がユニークに存在することを証明している補題。EmptyCollection VはVという型における∅がなんであるかを指定するものらしく、それが確かに空集合としての性質を持っているという補題がIsEmpty.empty。

ペア

次の定番は、ペアの公理を使ってシングルトンを作るやつである。ペアの公理に関しても、Z.leanでちゃんと整備されている。

noncomputable def doubleton (x y : V) : V := Classical.choose! (pairing_existsUnique x y)

@[simp] lemma mem_doubleton_iff {x y z : V} : z ∈ doubleton x y ↔ z = x ∨ z = y := Classical.choose!_spec (pairing_existsUnique x y) z

というわけで、シングルトンの存在も簡単に書ける。

example (x : V) : ∃ y : V, ∀ z : V, (z ∈ y ↔ z = x) := by
  use doubleton x x
  intro z
  constructor
  case h.mp =>
    intro h
    apply mem_doubleton_iff.mp at h
    cases h <;> assumption
  case h.mpr =>
    intro h
    rw [mem_doubleton_iff]
    left; exact h

そうなんだけど、これもZ.leanでもっときれいにやってくれてます。

noncomputable def singleton (x : V) : V := doubleton x x

noncomputable scoped instance : Singleton V V := ⟨singleton⟩

lemma singleton_def (x : V) : ({x} : V) = doubleton x x := rfl

@[simp] lemma mem_singleton_iff {x z : V} : z ∈ ({x} : V) ↔ z = x := by simp [singleton_def]

ちょっと注意しないといけないのは、{x}だけだと"typeclass instance problem is stuck"と言われてしまうので、({x} : V)と型を指定しないといけない点。

和集合

次の定番は和集合の公理を使ってトリプルトンを作るやつである。和集合についてもすでにZ.leanに書いてある。

noncomputable def sUnion (x : V) : V := Classical.choose! (union_existsUnique x)

prefix:110 "⋃ˢ " => sUnion

lemma mem_sUnion_iff {x z : V} : z ∈ ⋃ˢ x ↔ ∃ y ∈ x, z ∈ y := Classical.choose!_spec (union_existsUnique x) z

これだけ整備されていればトリプルトンも一発で記述できるから楽ちんである。

example : ∀ a b c : V, ∃ x : V, ∀ y : V, y ∈ x ↔ y = a ∨ y = b ∨ y = c := by
  intro a b c
  use sUnion (doubleton (doubleton a b) (singleton c))
  intro y
  constructor
  case h.mp =>
    intro hy
    apply mem_sUnion_iff.mp at hy
    obtain ⟨x, hx1, hx2⟩ := hy
    apply mem_doubleton_iff.mp at hx1
    obtain hab | hc := hx1
    case inl =>
      rw [hab] at hx2
      apply mem_doubleton_iff.mp at hx2
      obtain hya | hyb := hx2
      case inl =>
        left; exact hya
      case inr =>
        right; left; exact hyb
    case inr =>
      rw [hc] at hx2
      apply mem_singleton_iff.mp at hx2
      right; right; exact hx2
  case h.mpr =>
    intro h
    rw [mem_sUnion_iff]
    obtain hya | hyb | hyc := h
    case inl =>
      use doubleton a b
      simp only [mem_doubleton_iff, true_or, true_and]
      left; exact hya
    case inr.inl =>
      use doubleton a b
      simp only [mem_doubleton_iff, true_or, true_and]
      right; exact hyb
    case inr.inr =>
      use singleton c
      simp only [mem_doubleton_iff, or_true, true_and]
      apply mem_singleton_iff.mpr hyc

手数はかかっているけど自然でしょう。

次回予告

このままZ.leanを読みながらいろいろな集合を作ったりして遊んでいこうかと。

Combinations with repetition

Combinations with repetition

予告通りやる。

書き直し版allListsOfLength

combinationsとはなっているが、別の表現法で見た通り x_1+x_2+\cdots+x_n=kの非負整数解の個数と見たほうがわかりやすい。というわけで、allListsOfLengthにもう一度登場してもらう。のだが、今度はより直感的に書き直した。前回通り内包的な定義とも一致することが証明できる。

def allListsOfLength (s : Finset ℕ) (k : ℕ) : Finset (List ℕ) :=
  match k with
  | 0 => {[]}
  | k + 1 =>
      Finset.biUnion s
        (fun a => Finset.image (fun t => a :: t) (allListsOfLength s k))

構成

具体的にそういう解集合を作ってみる。当然ながら右辺がkなので左辺の変数の値となりうる最大の値もk、ということでFinset.range (k+1)で{0, 1, 2, ..., k}という集合を作ってここからリストを作ってもらう。その後で合計がkになるものだけをフィルターする。

def comb_w_rep (n k : ℕ) : Finset (List ℕ) :=
  (allListsOfLength (Finset.range (k + 1)) n).filter (fun s => s.sum = k)

これもallListsOfLengthを避けた内包的定義に一致することが証明できる。

lemma comb_w_rep_int (n k : ℕ) (l : List ℕ) : l ∈ comb_w_rep n k ↔
    l.length = n ∧ l.sum = k := by

l.sum=kかつa∈lならばa≦kを証明するのが面倒かなと思ったんだけど、それはすでに証明されている。こういうのがすぐ見つかるのはapply?の力で本当にありがたい。

theorem List.le_sum_of_mem{M : Type u_3} [AddMonoid M] [Preorder M] [CanonicallyOrderedAdd M] {xs : List M} {x : M} (h₁ : x ∈ xs) :
x ≤ xs.sum

ℕとかの具体的なものに対してではなく、加算モノイドで、preorderが存在して、それらに対して「足すと大きくなる」が言えるようなMに対して、List Mでそうなると言っている。型クラスもっと勉強しないとな。

分類とmodifyHead

帰納法でやるために、comb_w_rep (n + 1) (k + 1)を先頭が0かどうかで分ける。それを「comb_w_rep (n + 1) kのそれぞれの元の最初を1つ増やしたもの」と「comb_w_rep n (k + 1)の先頭に0を付け加えたもの」と解釈し直して和集合として表す。

lemma comb_w_rep_rec (n k : ℕ) : comb_w_rep (n + 1) (k + 1) =
    (comb_w_rep (n + 1) k).image (List.modifyHead (· + 1)) ∪
    (comb_w_rep n (k + 1)).image (List.cons 0) := by

List.modifyHead (· + 1)というのがリストの最初の元を一つ増やす関数になる。こういうときこんなふうに書くのかと思ったけど、冷静になってみたらラムダ式で書いても良かったのだった。まあとりあえずmodifyHeadは名前そのもののことをするなかなか便利な関数です。

l.sum=0

l : List ℕのとき、l.sum=0なら要素は全て0というのが面倒かなと思ったら最終的にきれいに書けたので証明まで乗せておく。

theorem listN_sum_zero_all_zero (l : List ℕ) (hl_sum : l.sum = 0) :
    l = List.replicate l.length 0 := by
  apply List.eq_replicate_of_mem
  intro a ha
  apply Nat.eq_zero_of_le_zero
  rw [← hl_sum]
  exact List.le_sum_of_mem ha

List.eq_replicate_of_memというのは、∀b∈l, b=aが成り立っていればl=List.replicate l.length bになるという定理。これをapplyしてやると、ゴールが∀b∈l, b=0になる。Nat.eq_zero_of_le_zeroはn : ℕに対して、n≦0ならばn=0という定理。これをapplyしてゴールをa≦0に変えたあとで、List.le_sum_of_memというリストの各元はリストの合計値を越えないという定理を使う。 ただ、定理を発見するところではhave使ってapply?してみたり、exampleでapply?してみたりしているので、なんかこうきれいにしてしまうと味気ない気もする。

フィニッシュ

切り分けも終わっているのであとは難しいことなく公式が導き出せたはず。

今後

とりあえずリハビリ終了ってことで、この後どうするかなぁ。角の三等分ができないってやつをやりたいいっている学生がいるのでそれをもう少し詳しく見るか、それとも去年やりたいと言っていて何もできていないFormalized Formal Logic · GitHubの集合論部分を読み始めるか。ちょっとここんとこ体調が絶望的で日常業務を回すだけで精一杯だったのです。 あー、でもそろそろLean以外のこともやらないとだしなぁ。

Combinations

Combinations

さて今度は組み合わせの方を。

べき集合

前回と同様、できる限りアルゴリズムで作るのではなく内包的にやりたい。組み合わせの方は基数が一定の部分集合全体と表せるわけで、まずは部分集合全体の集合、すなわちべき集合を作りたい。というか、それはすでに関数があってFinset.powerset sでFinset型のsのべき集合が書ける。

def Finset.powerset{α : Type u_1} (s : Finset α) : Finset (Finset α)

ついでなんで |P(S)|=2^{|S|}をやっておく。

theorem powerset_card (s : Finset ℕ) : s.powerset.card = 2 ^ (s.card) := by

これ、なんかえらく長いんだけど何やってんだろ(書いたの自分だろうに)。s.cardに関する帰納法をやるんだけど、そのままinduction s.cardとかやると帰納ステップが、

s : Finset ℕ
n✝ : ℕ
a✝ : s.powerset.card = 2 ^ n✝
⊢ s.powerset.card = 2 ^ (n✝ + 1)

とかになってダメ。s.cardに関する仮定すら抜ける。それを足すにはinduction hn : s.cardとかやる。すると、hn : s.card = n✝ +1というのがコンテクストに付け加わるんだが、もちろんa✝が不十分。これを何とかするには、sを全称化するgeneralizing sというのをつけて、induction hn : s.card generalizing sとやれば良い。いや、このあたりまだ正確にわかってないんだけどとりあえずこれで帰納ステップのコンテキストが

case succ
n✝ : ℕ
a✝ : ∀ (s : Finset ℕ), s.card = n✝ → s.powerset.card = 2 ^ n✝
s : Finset ℕ
hn : s.card = n✝ + 1
⊢ s.powerset.card = 2 ^ (n✝ + 1)

になってできそうな気がしてくる。あとは、sが空でないことを使って元を取ってきてそれが入っているかいないかでべき集合を分類してそれぞれ数えるだけなんだけどなぁ。なぜこんなに長い。

Combinations

組み合わせの定義はこうなる。ついでなので集合内包表記を使ってみた。

def combination (s : Finset ℕ) (k : ℕ) : Finset (Finset ℕ) :={t ∈ s.powerset | t.card = k}

C(n, k)

さて数を数えよう。べき集合のときと同様に、s.cardに関する帰納法。k=0のときは空集合しかないというので切り抜け、あとはa∈sを取ってきて、それが入っているかいないかで分けて帰納法の仮定を使う。切り分け部分のステートメントはこう。

lemma combination_rec (s : Finset ℕ) (k : ℕ) (a : ℕ) (ha : a ∈ s) : 
    combination s (k + 1) = combination (s.erase a) (k + 1) ∪
    Finset.image (fun t ↦ t ∪ {a}) (combination (s.erase a) k) 

それを使って最後まで解ける。

lemma combination_card (n : ℕ) (k : ℕ) (hk : k ≤ n) (s : Finset ℕ)
    (hs : s.card = n) : (combination s k).card = Nat.choose n k := by

Finset.card_union

もうすでに使ったかもしれないけど、下記の定理が使われる。英語だとInclusion-exclusion principleだけど日本語だと包除原理っていうの?

theorem Finset.card_union{α : Type u_1} [DecidableEq α] (s t : Finset α) :
(s ∪ t).card = s.card + t.card - (s ∩ t).card

当然ながらsとtがdisjointなら最後が消えるので、その場合のも使える。

Finset.eq_iff_card_ge_of_superset

真面目に証明しないといけないかと思っていたのがすでに証明されていた。有限集合の部分集合が全体とサイズが同じなら全体そのものってやつ。

theorem Finset.eq_iff_card_ge_of_superset{α : Type u_1} {s t : Finset α} (hst : s ⊆ t) :
t.card ≤ s.card ↔ t = s

ソース

ここでやってること、ソースはここで公開してますのでよろしかったらどうぞ。 github.com

次回予告

アメリカの離散数学の教科書ではこのあたりのあとに定番として"combinations with repetition"というのをやる。なにかを選択するときに一個とは限らない組み合わせという言い方で、例えばドーナツの味が3種類あってそれを4個買う場合のやり方はいくつとかいうのになる。もっと数学的にわかりやすい言い方は、 x_1+x_2+\cdots+x_n=kという数式に何個非負整数な解があるかというものとなって、これがC(n+k-1, k)に等しくなる。これをやりたい。というか、すでにできてる。アメリカの離散数学の教科書にはこの2つを一対一対応させる面白い証明が書いてあるんだけど、そうじゃなくて帰納法で証明している。次回はそれについて書いて、数え上げの話は一旦おしまいにしたい。

Lean 4 アップデート

Lean 4.31.0

予告した通りで。Leanの新しいstableなバージョンである4.31.0がリリースされたそうなので、試しにlean-studyをアップデートしてみようとしてコケた話をする。

経緯。

  1. VS Code上で、∀のアイコンを押して、Project: Update Dependency...→mathlibとして、Update Lean Versionというボタンが出たらクリックしたらできるとGeminiさんに言われたからやる。

  2. lean-toolchain(現在のLeanバージョンを指定するファイル)が書き換わってないよと言ったら、じゃあTerminalでやればと言われて、Git Bashを起動しカレントフォルダをプロジェクトのフォルダに持っていって、"curl -L https://raw.githubusercontent.com/leanprover-community/mathlib4/master/lean-toolchain -o lean-toolchain", "lake update", "lake exe cache get" とやればよいと言われる。

  3. lake updateの途中でelanの新しいバージョンが出たから"elan self update"でアップデートしてねとか言われて、その後しばらくしたらエラーを出して止まる。

  4. そんじゃあelanを先にアップデートしようとするが"elan self update"がエラーを出して止まる。GeminiさんいわくVS Codeが動いているからではないかと言われたので全部閉じてもう一度。今度は成功。

  5. Geminiさんに"lake clean", "lake update", "lake exe cache get"をやれと言われたのでやるが、また同じエラーで止まる。

  6. これはGeminiさんがバカなのかと思い始め、Claude Sonnet 4.6に聞いてみる。"lake build Cache.IO"とピンポイントでエラーを出している個所をやってみたらどうかと言われて、その出力を渡して聞いてみると、MathlibとLeanのバージョンミスマッチだから、"rm -rf ./lake"やってから、"lake update", "lake exe cache get"とやれと言われる。また同じエラーで止まる。

  7. どこかで自動的に変更してくれると思っていたlakefile.tomlのRequire name ="mathlib"のところにrev="4.30.0"と書いてあるのを見つける。Geminiさんにそう伝えると、rev="master"と書き直すといいよと言われる。

  8. "lake clean", "lake update"がようやく通り、"lake exe cache get"も問題なく終わり、"lake build"はちゃんと瞬速で終わった。

というわけで、おそらくはelanが最初にアップデートされていなかったこと、あとlakefile.tomlのrevが"v4.30.0"になっていたのが問題なのだろうと思う。

破壊的変更

いくつかこれまでのコードが通らないような変更があるらしいが、ほとんどは私には関係ない。唯一起こったのが、case名が変わったことで、casesとかconstructorとか複数のゴールができるtacticではそれぞれに名前がついて、それは無視しても問題ないのだけれども、私は情報が増えていいなと思ってちゃんとcase leftとかcase righとか書いていた。この自動生成される名前がちょっと違うときがある。まあ正しい名前はInfoviewに載っているので変更は容易。

Permutation

Permutation

定義

まずこの前言ったように、k-permutation of {0, 1, 2, ..., n-1}は、{0, 1, 2, ..., n-1}の要素からなる長さkのリストで重複がないものとして定義する。リストlが重複がないことを示すList.Nodupという関数があるのでそれを使う。

def permutation (s : Finset ℕ) (k : ℕ) : Finset (List ℕ) := (allListsOfLength s k).filter (fun l ↦ l.Nodup)

前回の古典論理が云々のところで書いた通り、filterをかけるための述語はDecidableでないといけないとかあるんだけど、ここは List ℕだから問題にならない、ということのはず。 あ、修正せずに出しちゃっているけど、現在のLeanでの推奨はfun x ↦ yではなく、fun x => yなんだそうです。

内包的定義

まあfilterで定義しているんだからすでに内包的みたいなもんだけど、allListsOfLengthの方もまとめて平易な同値条件を示しておく。

theorem permutation_int (s : Finset ℕ) (k : ℕ) (l : List ℕ) : l ∈ permutation s k ↔ l.length = k ∧ (∀ a ∈ l, a ∈ s) ∧ l.Nodup := by

これの証明はまあ手癖でいけるレベルなんだけど、今見ると同値が仮定されているときはrwの方が便利だからそっちにするべきだったかなとは思う。

再帰的定義

k-permutationの個数を数えるために再帰的な定義が可能であるということを証明する。

lemma permutation_rec (s : Finset ℕ) (k : ℕ) :
    permutation s (k + 1) = Finset.biUnion s
    (fun a ↦ Finset.image (fun as ↦ a :: as ) (permutation (s.erase a) k))

Finset.biUnionで記述しているのは、sのそれぞれの元aに対して、{a :: as | as∈permutation (s.erase a) k}をとってきてそれらの和集合を取ったもの。つまりは、最初の元によって分類している。 ここで初めて使ったtacticとしてextというのがあって、これは外延性をできる限り適用していくというやつ。だから、この補題の証明の初っ端でext lをやるとl : List ℕがコンテキストに追加になり、ゴールが

l ∈ permutation s (k + 1) ↔ l ∈ s.biUnion fun a ↦ Finset.image (fun as ↦ a :: as) (permutation (s.erase a) k)

になる。一回分ならapply Finset.ext_iff.mprのあとでintro lすれば同じ状態になるけど、ext lだと1ステップ得する。

左から右はlをa::asと表せばすぐ行ける。逆側はこの和集合の元がNodupを満たすことを言わないといけないんだけれども、それらはas∈permutation (s.erase a) kを満たすaとasの組に対するa::asとなって、この状態でsimpするとa∉as∧as.Nodupに分解してくれる。本当にsimp?とapply?ばっかりやってる。

主定理

ここまで来たらあとは楽ちん、のはずなんだけどなんかLean書くの下手くそ選手権に出れそうな証明になっちゃっているのであとでなんとかするかもしれない。式の変形で解く部分が5連続haveとかになっていて、いくら眠かったとはいえひどすぎだ。

theorem permutation_card (s : Finset ℕ) (k : ℕ) :
    (permutation s k).card = (s.card).descFactorial k := by

Nat.descFactorial n kの定義はn(n-1)(n-2)...(n-k+1)なのでまさにpermutation公式のやつ。 ここで覚えたtacticがrevert。introの逆をやるようなtacticで、この定理の証明の最初にrevert sとやると、この状態になる。

k : ℕ
⊢ ∀ (s : Finset ℕ), (permutation s k).card = s.card.descFactorial k

どうしてこうしたいかと言うとkに対する帰納法でこれを証明したいから。permutation s (k+1)のケースを考えるときに、sの元aに対してpermutation (s.erase a) kの基数がs.card.descFactorial kであることが帰納法の仮定から示せる状況を作りたいので。っていうか、そもそもFinsetの定義が複雑なのでこれは直接帰納法にかけられない。 ちょっとした変化はあるけれども、だいたいのテンプレはallListsOfLength_cardのときにできているのであまり問題はない。

次回予告: バージョンアップ

Lean 4の新しいstableなバージョンであるv4.31.0が出たのでバージョンアップした。なんかけっこうハマったのでそれを赤裸々に書く予定。もちろん殆どは私の単純ミスであるが、Geminiさんがもう少し賢く教えてくれても良かったのではないかと思う。

そのために、バージョンアップ前に書いたコードが少しだけ動かなかった。そのことも書きたい。

Classical.choice

古典論理

by_contra

そんなことも知らないで今までLeanやってたのかって言われそうだが。これまで何も考えずにby_contra tactic使ってたんすよ。よく考えなくてもこれは直観論理から外れるんであって、Leanって直観論理が基本じゃないのとか思いつつ気にしてなかった。 んで一念発起してGeminiさんに聞いてみたんすよ。あ、このたぐいで基本Geminiを使っているのは大学が契約していて無制限で使えるからで、それ以上の思い入れがあるわけではない。曰く「ああ、もちろんby_contraは基本的に古典論理が必要だよ。だから、これを使って証明されたやつに#print axiomsをかけるとClassical.choiceが含まれているよ」と。 なんすかその#print axiomsってのは??とりあえず試してみる。

theorem double_negation (P : Prop) (h : ¬¬P) : P := by
  by_contra
  contradiction

#print axioms double_negation

とすると、InfoViewに"'double_negation' depends on axioms: [propext, Classical.choice, Quot.sound]"と表示される。 もちろん逆には古典論理がいらない。つまり、

theorem safe_one (P : Prop) (h : P) : ¬¬P := by
  intro h1
  contradiction

#print axioms safe_one

とすると、"'safe_one' does not depend on any axioms"となる。 常日頃、「私はset theoristであってlogicianではない」と公言しているにしてもこれはいくらなんでもひどすぎる。ごめんなさい。

Finset.filter

Finsetは基本的に計算可能であろうとしている型なので、Finset.filterは述語が決定可能であることを要求している。

def Finset.filter{α : Type u_1} (p : α → Prop) [DecidablePred p] (s : Finset α) :
Finset α

そのそのことはMathematics in Leanにも書いてあって、下記のような例が挙げられている。

variable {α : Type*} (a : α) (s t : Finset α)

#check a ∈ s
#check s ∩ t

これをやるとs ∩ tのところで、下記のメッセージが出る。

failed to synthesize instance of type class
  Inter (Finset α)

このメッセージが意味するところはわからないのだけれども、とにかく共通部分が書けない。Set型はそういうの気にしないので、(s t : Set α)というようにsとtの型を変更してやれば通る。 「俺はFinsetを古典論理的に扱いたいんだ」という場合にはopen Classical inを対象となる定理とか定義の直前に書くとそこでは古典論理的に処理してくれる。セクションごと古典論理的にするには、下記のようにセクションを始めれば良いらしい。

open Classical in
noncomputable section

こうやって始めたセクションで上記のコードをやると共通部分のところも通る。なお、"open Classical"とするとその後ずっと古典論理になるのだけれども、これはlinterに怒られる。

積極的に無知をさらしていくスタイル

湯川先生レスペクトで。

次回予告を無視するスタイル

そういえば次回予告で予告をやらないのはよく見るけど、次回予告で予告したのと本編が違うというのは見たことないな。さすがに怒られるのか。 とりあえずここまで書いたら時間切れになってしまったのでpermutationは次回に。実はcombinationの方も終わっているのだけれども。