This time, we'll make MyInt usable as an ordered set. We'll start by introducing preorders, set up a cast from MyNat to MyInt, define the non-strict and strict orders, and finally register MyInt as an instance of Mathlib's Preorder type class so that the order tactic becomes available. This article follows along with "ゼロから始めるLean言語入門 手を動かして学ぶ形式数学ライブラリ開発".
What is a preorder?
A binary relation on a set X is called a preorder when it satisfies the following two properties.
Reflexivity: \( \forall x \in X,\ x \le x \)
Transitivity: \( \forall x, y, z \in X,\ x \le y \land y \le z \to x \le z \)
MyInt satisfies both of these properties, so it can be treated as a preorder. We'll register it as an instance of Mathlib's Preorder type class.
Treating MyNat as MyInt
Before defining the order, we need a way to treat natural numbers as integers. Currently, passing a MyNat directly to a function expecting MyInt results in an error.
private def MyInt.const (z : MyInt) : MyInt → MyInt := fun _ => z
#check_failure MyInt.const (0 : MyNat)
It needs to be converted before being passed in as MyInt, like this.
def MyInt.ofMyNat (n : MyNat) : MyInt := ⟦(n, 0)⟧
#check MyInt.const (.ofMyNat 0)
Using Lean 4's type coercion mechanism, we can force MyNat to convert into MyInt.
instance : Coe MyNat MyInt where
coe := MyInt.ofMyNat
-- (MyInt.ofMyNat 0).const : MyInt → MyInt
#check MyInt.const (0 : MyNat)
However, with this approach the conversion always happens forcibly, rather than acting as a genuine cast.
example : ((0 : MyNat): MyInt) = (0 : MyInt) := by
guard_target =ₛ MyInt.ofMyNat 0 = 0
sorry
Types that are in a subset relationship, like MyNat and MyInt, are more convenient to treat as casts. This can be achieved by registering MyInt.ofMyNat with the coe attribute.
attribute [coe] MyInt.ofMyNat
Looking at the example again, we can see that MyNat is now treated as a cast, written ↑(0 : MyNat). The ↑ symbol indicates a type cast.
@[simp]
theorem MyInt.ofMyNat_zero_eq_zero : MyInt.ofMyNat 0 = (0: MyInt) := by
dsimp [MyInt.ofMyNat]
rfl
example : ((0 : MyNat): MyInt) = (0 : MyInt) := by
guard_target =ₛ ↑(0 : MyNat) = (0 : MyInt)
simp
Proving the cast is injective
Next we prove that this cast is injective. The norm_cast attribute registers a lemma with the tactics that handle type casts.
@[norm_cast]
theorem MyInt.ofMyNat_inj {m n : MyNat}
: (m : MyInt) = (n : MyInt) ↔ m = n := by
constructor <;> intro h
case mp =>
have : (m, 0) ≈ (n, 0) := by
exact Quotient.exact h
notation_simp at this
simp_all
case mpr =>
rw [h]
The norm_cast tactic can then be used as follows.
@[simp]
theorem MyInt.ofMyNat_eq_zero {n : MyNat} : (n : MyInt) = 0 ↔ n = 0 := by
constructor <;> intro h
· rw [show (0 : MyInt) = ↑ (0 : MyNat) from rfl] at h
norm_cast at h
· simp_all
Registering the push_cast attribute makes casts get pushed inward before tactics like ring run, expanding the cast into the inside of the parentheses.
@[push_cast]
theorem MyInt.ofNat_add (m n : MyNat)
: ↑ (m + n) = (m : MyInt) + (n : MyInt) := by
rfl
Defining non-strict and strict order on MyInt
We define the non-strict order (≤) and strict order (<) on MyInt.
def MyInt.le (m n : MyInt) : Prop :=
∃ k : MyNat, m + ↑ k = n
instance : LE MyInt where
le := MyInt.le
@[notation_simp]
theorem MyInt.le_def (m n : MyInt) : m ≤ n ↔ ∃ k : MyNat, m + ↑ k = n := by
rfl
def MyInt.lt (m n : MyInt) : Prop :=
m ≤ n ∧ ¬ n ≤ m
instance : LT MyInt where
lt := MyInt.lt
@[notation_simp]
theorem MyInt.lt_def (a b : MyInt) : a < b ↔ a ≤ b ∧ ¬ (b ≤ a) := by
rfl
Proving reflexivity
@[refl]
theorem MyInt.le_refl (m : MyInt) : m ≤ m := by
exists 0
simp
Proving transitivity
Next we prove transitivity. The key steps are rewriting into sum form with notation_simp, turning the goal into ⊢ n + ↑(a + b) = k with use a + b, and then into ⊢ n + (↑a + ↑b) = k with push_cast. From there, the hypotheses and ac_rfl get us to the goal.
theorem MyInt.le_trans {m n k : MyInt} (hnm: n ≤ m) (hmk: m ≤ k) : n ≤ k := by
notation_simp at *
obtain ⟨a, ha⟩ := hnm
obtain ⟨b, hb⟩ := hmk
-- a : MyNat
-- ha : n + ↑a = m
-- b : MyNat
-- hb : m + ↑b = k
-- ⊢ ∃ k_1, n + ↑k_1 = k
use a + b -- ⊢ n + ↑(a + b) = k
push_cast -- ⊢ n + (↑a + ↑b) = k
have : n + (↑ a + ↑ b) = k := calc
_ = (n + ↑ a) + ↑ b := by ac_rfl
_ = m + ↑ b := by rw [ha]
_ = k := by rw [hb]
assumption
We register transitivity with the Trans class so it can be used with the calc tactic.
instance : Trans (· ≤ · : MyInt → MyInt → Prop) (· ≤ ·) (· ≤ ·) where
trans := MyInt.le_trans
Registering as a Preorder
Finally, we register MyInt as a preorder so that the order tactic becomes available.
instance : Preorder MyInt where
le_refl := MyInt.le_refl
le_trans := by
intro a b c hab hbc
apply MyInt.le_trans hab hbc
example (a b c : MyInt) (h1: a ≤ b) (h2 : b ≤ c) : a ≤ c := by
order
With the Preorder instance registered, the order tactic can now handle reflexivity and transitivity automatically.
The code is available in the GitHub repository.