Lean 4でMyIntを順序環として登録する

2026年8月16日日曜日

Lean 技術記事 数学

t f B! P L

前回までの記事で、MyIntが半順序集合(≈ partial order)であることを示しました。今回はその続きとして、MyIntが全順序であること、さらに順序環であることを証明していきます。この2つの性質が揃うと、Mathlibのlinarithやnlinarithといった強力な自動証明タクティクが使えるようになり、不等式を含む証明が一気に書きやすくなります。今回の内容はゼロから始めるLean言語入門 手を動かして学ぶ形式数学ライブラリ開発の該当章に沿っています。

全順序とは

集合Xにおける二項関係が全順序であるとは、次の2つを満たすことをいいます。

  • Xがその二項関係について半順序集合である
  • \( \forall x, y \in X,\ x \le y \lor y \le x \)

半順序は「比較できない要素があってもよい」順序でしたが、全順序ではどの2つの要素をとっても必ずどちらかがどちらか以下になります。MyIntはすでに半順序であることがわかっているので、あとは任意の2元が比較可能であることさえ示せば全順序であることが言えます。

0 ≤ a ∨ 0 ≤ -a の証明

いきなり\( a \le b \lor b \le a \)を狙うのではなく、まずは\( 0 \le a \lor 0 \le -a \)という補題を用意します。MyIntは\( m - n \)の形の商型として定義されているので、Quotient.inductionOnで代表元のペア(m, n)を取り出せば、話はMyNatの世界の大小比較に帰着します。MyNatはすでに全順序(MyNat.le_total)であることを証明済みなので、そこからn ≤ mかm ≤ nのどちらかが言え、あとはsimp [mk_def]とnorm_castでMyIntの言葉に翻訳するだけです。

theorem MyInt.nonneg_or_nonneg_neg {a : MyInt} : 0 ≤ a ∨ 0 ≤ -a := by
  induction a using Quotient.inductionOn with
  | h a =>
    obtain ⟨m, n⟩ := a
    have : n ≤ m ∨ m ≤ n := by
      exact MyNat.le_total n m

    cases this with
    | inl h =>
      left
      simp [mk_def]
      norm_cast
    | inr h =>
      right
      simp [mk_def]
      norm_cast

MyInt.le_total の証明

本題の\( a \le b \lor b \le a \)は、\( 0 \le b - a \lor 0 \le -(b - a) \)という形に置き換えると、先ほどの補題がそのまま使えます。ここでsufficesタクティクを使い、「この形が示せればゴールも示せる」ことを先に片付けてから、実際にその形を証明する流れです。\( 0 \le b - a \)から\( a \le b \)への変換には前回定義したMyInt.sub_nonnegを使い、もう片方はabelタクティクで\( -(b-a) = a - b \)という可換群の計算を自動で処理させています。

theorem MyInt.le_total (a b : MyInt) : a ≤ b ∨ b ≤ a := by
  suffices goal : 0 ≤ b - a ∨ 0 ≤ -(b -a) from by
    cases goal with
    | inl h =>
      left
      rw [← MyInt.sub_nonneg]
      assumption
    | inr h =>
      right
      rw [← MyInt.sub_nonneg]
      rw [show -(b -a) = a - b from by abel] at h
      assumption
  exact MyInt.nonneg_or_nonneg_neg

LinearOrderへの登録

全順序であることが証明できたので、MathlibのLinearOrder型クラスにMyIntを登録します。le_totalフィールドに先ほどの定理を渡すだけで済み、決定可能性(toDecidableLE)はすでにあるDecidableLEインスタンスからinfer_instanceで自動導出できます。

instance : LinearOrder MyInt where
  le_total := MyInt.le_total
  toDecidableLE := by infer_instance

この登録によって、orderタクティクが使えるようになります。orderタクティクは、≤・<・=に関する既知の事実から、全順序の公理だけを使って導ける結論を自動で探してくれるタクティクです。例えば次のように、場合分けの一部をorderに任せることができます。

theorem MyInt.eq_or_lt_of_le {a b : MyInt} (h : a ≤ b) : a = b ∨ a < b := by
  by_cases hor : a = b
  case pos =>
    left
    assumption
  case neg =>
    right
    order

theorem MyInt.le_of_eq_or_lt {a b : MyInt} (h : a = b ∨ a < b) : a ≤ b := by
  cases h with
  | inl h => rw [h]
  | inr h => order

theorem MyInt.le_iff_eq_or_lt (m n : MyInt) : n ≤ m ↔ n = m ∨ n < m := by
  constructor
  case mp => apply MyInt.eq_or_lt_of_le
  case mpr => apply MyInt.le_of_eq_or_lt

順序環とは

次はMyIntが順序環であることを示します。順序環とは、集合Xとその上の2つの二項演算が環をなし、さらに次の2条件を満たすもののことです。

  • \( \forall x, y, z \in X,\ x \le y \to x + z \le y + z \)
  • \( \forall x, y \in X,\ 0 \le x \land 0 \le y \to 0 \le x \cdot y \)

つまり、順序が加法とは常に整合し、乗法とは非負の範囲で整合する、という性質です。これをMathlibの型クラスとして登録することが目標です。

狭義順序を加法の式へ変換する

MyIntの狭義順序\( a < b \)は\( a \le b \land \lnot (b \le a) \)として定義されています。これから先の証明では、この論理式のままだと扱いにくい場面が多いため、\( a < b \)を「\( b \)は\( a \)にMyNatの正の値を足したものである」という形、つまり\( \exists k : \text{MyNat},\ a + (k + 1) = b \)に書き換えられることを証明しておきます。こうしておくと、後の証明でMyNat側の帰納法や具体的な値の計算がそのまま使えるようになります。

lt.destでは、まずnotation_simpで\( a < b \)の定義を展開し、\( a \le b \)の中身である\( \exists k, a + k = b \)からkを取り出します。もしk=0なら\( a = b \)となってしまい、これは\( \lnot (b \le a) \)という仮定と矛盾するので、kは0ではなく、必ず何かの後者\( k+1 \)の形になっている、という筋道です。逆向きのlt.introでは、証拠となる\( a + (k+1) = b \)から\( a \le b \)を作るのは簡単ですが、\( \lnot (b \le a) \)の証明がやや技巧的で、もし\( b \le a \)だったとすると\( 1 \le 0 \)という矛盾が\( \text{calc} \)ブロックの計算で導かれ、最後はorderタクティクで締めています。

variable {a b c : MyInt}

theorem MyInt.lt.dest (h : a < b) : ∃ k : MyNat, a + (↑k + 1) = b := by
  notation_simp at h
  -- a b : MyInt
  -- h : (∃ k, a + ↑k = b) ∧ ¬∃ k, b + ↑k = a
  -- ⊢ ∃ k, a + (↑k + 1) = b
  obtain ⟨⟨k, hk⟩, h⟩ := h
  -- h : ¬∃ k, b + ↑k = a
  -- k : MyNat
  -- hk : a + ↑k = b
  -- ⊢ ∃ k, a + (↑k + 1) = b
  induction k with
  | zero =>
    exfalso
    replace hk : a = b := by simp_all
    have : ∃ k : MyNat, b + ↑k = a := by
      rw [hk]
      exists 0
      simp
    contradiction
  | succ k _ =>
    push_cast at hk
    use k
    assumption

theorem MyInt.le.intro (a : MyInt) (b : MyNat) : a ≤ a + ↑b := by
  exact ⟨b, rfl⟩

theorem MyInt.lt.intro (h : ∃ k : MyNat, a + (k + 1) = b) : a < b := by
  obtain ⟨k, hk⟩ := h
  simp only [lt_def] -- ⊢ a ≤ b ∧ ¬b ≤ a
  constructor

  case left => -- ⊢ a ≤ b
    notation_simp
    use k + 1
    assumption
  case right => -- ⊢ ¬b ≤ a
    notation_simp
    intro ⟨s, hs⟩
    rw [← hs] at hk
    -- hk : a + (↑k + 1) = b
    -- hs : b + ↑s = a
    -- ⊢ a + (↑k + 1) = b
    have : ↑(s + k) + (1 : MyInt) = 0 := calc
      _ = (↑s + ↑k) + (1 : MyInt) := by push_cast; ac_rfl
      _ = (b + ↑s + (↑k + 1)) - b := by abel
      _ = b - b := by rw [hk]
      _ = 0 := by abel

    replace : (0 : MyInt) > 0 := calc
      _ = ↑(s + k) + (1 : MyInt) := by rw [this]
      _ = (1 : MyInt) + ↑(s + k):= by ac_rfl
      _ ≥ (1 : MyInt) := by apply MyInt.le.intro
      _ > (0 : MyInt) := by decide
    order

theorem MyInt.lt_iff_add : a < b ↔ ∃ k : MyNat, a + (k + 1) = b := by
  constructor
  case mp => exact MyInt.lt.dest
  case mpr => exact MyInt.lt.intro

掛け算が狭義順序を保つことの証明

次に\( a < b \land 0 < c \to c \cdot a < c \cdot b \)を示します。まず準備として、MyNatの掛け算がMyIntへのキャストと可換であることを示すofMyNat_mulを@[push_cast]属性付きで登録しておきます。これにより以降の証明でpush_castタクティクがこの等式を自動的に使ってくれるようになります。

mul_posでは、\( 0 < a \)と\( 0 < b \)をそれぞれ先ほどの\( \exists k, \dots \)の形に展開し、\( a = 0 + (c+1) \)、\( b = 0 + (d+1) \)と書き換えたうえで、積の証拠として\( c \cdot d + c + d \)を提示します。あとはpush_castとringで両辺が等しいことを機械的に確認するだけです。sub_posは\( 0 < a - b \leftrightarrow b < a \)という、引き算と大小関係を結びつける補題で、これを使うと\( c \cdot a < c \cdot b \)の証明を「\( 0 < c \cdot (b-a) \)を示せばよい」というmul_posが使える形に帰着できます。

@[push_cast]
theorem MyInt.ofMyNat_mul (m n : MyNat) : ↑(m * n) = (m : MyInt) * (n : MyInt) := by
  dsimp [MyInt.ofMyNat]
  apply Quotient.sound
  notation_simp
  ring

theorem MyInt.mul_pos (ha : 0 < a) (hb : 0 < b) : 0 < a * b := by
  rw [MyInt.lt_iff_add] at *
  obtain ⟨c, hc⟩ := ha
  obtain ⟨d, hd⟩ := hb
  -- hc : 0 + (↑c + 1) = a
  -- hd : 0 + (↑d + 1) = b
  -- ⊢ ∃ k, 0 + (↑k + 1) = a * b
  rw [← hc, ← hd] -- ⊢ ∃ k, 0 + (↑k + 1) = (0 + (↑c + 1)) * (0 + (↑d + 1))
  use c * d + c + d
  push_cast
  ring

theorem MyInt.sub_pos : 0 < a - b ↔ b < a := by
  constructor <;> intro h
  · rw [MyInt.lt_iff_add] at *
    obtain ⟨k, hk⟩ := h
    simp at hk
    use k
    rw [hk]
    abel
  · rw [MyInt.lt_iff_add] at *
    obtain ⟨k, hk⟩ := h
    use k
    rw [← hk]
    abel

theorem MyInt.mul_lt_mul_of_pos_left (h : a < b) (pos : 0 < c)
  : c * a < c * b := by
  suffices 0 < c * (b - a) from by
    -- h : a < b
    -- pos : 0 < c
    -- this : 0 < c * (b - a)
    -- ⊢ c * a < c * b
    rw [MyInt.lt_iff_add] at this ⊢
    -- this : ∃ k, 0 + (↑k + 1) = c * (b - a) ⊢ ∃ k, c * a + (↑k + 1) = c * b
    obtain ⟨k, hk⟩ := this
    simp at hk
    use k
    rw [hk]
    simp [MyInt.left_distrib]
  replace h : 0 < b - a := by
    rw [MyInt.sub_pos]
    assumption
  apply MyInt.mul_pos (ha := pos) (hb := h)

theorem MyInt.mul_lt_mul_of_pos_right (h : a < b) (pos : 0 < c)
  : a * c < b * c := by
  rw [MyInt.mul_comm a c, MyInt.mul_comm b c]
  apply MyInt.mul_lt_mul_of_pos_left (h := h) (pos := pos)

instance : IsStrictOrderedRing MyInt where
  zero_le_one := by decide
  exists_pair_ne := by exists 0, 1
  mul_lt_mul_of_pos_left := by
    intro a ha b c hbc
    exact MyInt.mul_lt_mul_of_pos_left hbc ha
  mul_lt_mul_of_pos_right := by
    intro c hc a b hab
    exact MyInt.mul_lt_mul_of_pos_right hab hc

最後にこれらをまとめてIsStrictOrderedRing型クラスに登録します。このクラスは環の演算と狭義順序が整合していることを表すもので、zero_le_one(0と1の大小)やexists_pair_ne(異なる2元が存在すること、つまり自明でない環であること)なども要求されます。

linarithタクティクの利用

順序環として登録できたことで、linarithタクティクが使えるようになります。linarithは、仮定と結論がすべて線形な等式・不等式で表せるとき、それらを組み合わせて矛盾や結論を自動で導いてくれるタクティクです。手で式変形をする必要がなくなるため、非常に強力です。

example (h : a < b) : a + c < b + c := by
  linarith

example (h1 : 2 * a - b = 1) (h2 : a + b = 5) : a = 2 := by
  linarith

ただし実際に使ってみると、数値リテラル(2や5など)の扱いでうまくいかないケースがありました。原因は、MyIntのCommRingインスタンスにおいて自然数キャストの後者に関する規則(natCast_succ)が正しく設定されていなかったことにあります。そこでMyInt.ofNatが後者演算と整合することを示すofNat_succを用意し、CommRingインスタンスに明示的に組み込みました。

theorem MyInt.ofNat_succ (n : Nat) : MyInt.ofNat (n + 1) = MyInt.ofNat n + 1 := by
  apply Quotient.sound
  notation_simp
  simp [MyInt.ofNat, MyNat.ofNat]
  change (MyNat.ofNat n).succ = MyNat.ofNat n + MyNat.succ 0
  exact (MyNat.add_succ (MyNat.ofNat n) 0).symm

instance : CommRing MyInt where
  natCast := MyInt.ofNat
  natCast_zero := rfl
  natCast_succ := MyInt.ofNat_succ

掛け算の広義順序とnlinarith

最後に広義順序\( 0 \le c \land a \le b \to c \cdot a \le c \cdot b \)を示します。これはこれまでに証明した狭義順序の性質やその他の環・順序の公理から論理的に導けるのですが、ここでは手で証明を組み立てる代わりにnlinarithタクティクに任せています。nlinarithはlinarithを拡張し、仮定同士の積など簡単な非線形の組み合わせも試したうえで線形な矛盾を探してくれるタクティクで、掛け算が絡む今回のような主張にも直接使えます。

theorem MyInt.mul_le_mul_of_nonneg_left (h : a ≤ b) (nneg: 0 ≤ c)
  : c * a ≤ c * b := by
  nlinarith

theorem MyInt.mul_le_mul_of_nonneg_right (h : a ≤ b) (nneg: 0 ≤ c)
  : a * c ≤ b * c := by
  nlinarith

instance : IsOrderedRing MyInt where
  zero_le_one := by decide
  mul_le_mul_of_nonneg_left := by
    intro a ha b c hbc
    exact MyInt.mul_le_mul_of_nonneg_left hbc ha
  mul_le_mul_of_nonneg_right := by
    intro c hc a b hab
    exact MyInt.mul_le_mul_of_nonneg_right hab hc

example (h1 : 3 * a - 2 * b = 5) (h2: 6 * a - 5 * b = 11)
  : b = -1 := by nlinarith

これでIsOrderedRing型クラスへの登録も完了し、nlinarithだけで非線形な等式・不等式の証明が書けるようになりました。

まとめ

今回はMyIntが全順序であることと、順序環であることを証明し、LinearOrder・IsStrictOrderedRing・IsOrderedRingの3つの型クラスに登録しました。これにより、order・linarith・nlinarithという強力な自動証明タクティクがMyIntに対しても使えるようになっています。教科書にあるgrindタクティクについては本シリーズでは扱いませんが、これで教科書の内容はすべて終了となります。

コードの全体はhttps://github.com/ringring-creator/LearnLeanで公開しています。

このブログを検索

Smart Hide Launcher の名称を Hitoma Launcher に変更しました

Smart Hide Launcher の名称を、Hitoma Launcher に変更しました。アプリアイコンも新しくなります。 コンセプトは「必要なアプリだけ」です。 これまでの名前は、アプリを「隠す」という機能を中心に表していました。今回の名称変更では、必要なアプリを選...

QooQ