Quotient

Quotient は、型 α 上の同値関係 r : Setoid α による 商(quotient) を表します。Setoid を受け取って、商型を返します。

商とは

商とは、ある二項関係 r : α → α → Prop によって同じと見なされるものを同一視したものです。たとえば三角形という図形を考えるとき、回転や平行移動させたものを我々は「同じ形」と認識しますが、これは「回転と平行移動で重なり合う」という二項関係に関する商を取っていることになります。

そもそも「2つのリンゴ」や「2つの消しゴム」といった物の集まりに対して「2個の物の集まりである」という共通点を見出すこと自体、商を取る操作です。この場合は、集合に対して「1対1で漏れのない対応が存在する」という二項関係を考えて商を取ったことになります。したがって、自然数という概念を受け入れた時点で、私たちは商を取るというアイデアを受け入れたことになります。

商がいかに受け入れがたく感じられようと、商は基本的で直観に根ざした操作であると言えます。

Quotient に関する基本的な操作

Quotient.mk: 同値類を取る操作

α 上の同値関係 r : α → α → Prop があるとき、r によって同じと見なされる項同士のグループのことをその項の 同値類(equivalence class) と呼びます。たとえば、整数全体 において r x y := 4 ∣ (x - y) と定義すると r は同値関係になりますが、これによる 0 の同値類は 4 で割った余りがゼロになる項の全体、つまり {0, ±4, ±8, ±12, ..} という集合になります。

Leanで項 x : α に対して、同値関係 sr : Setoid α による同値類を取る操作は Quotient.mk で表されます。

variable {α : Type} (sr : Setoid α)

#check (Quotient.mk sr : α → Quotient sr)

元の型 α は、すべての項がどれかの項の同値類に属していて、複数の同値類に属する項はないので、同値類たちの直和として表されます。そこで同値類の全体のことを α 上の同値関係 sr : Setoid α による 商(quotient) と呼びます。Quotient が表すのは、まさにこの商です。同値類を取る操作 Quotient.mk は、商への関数になります。これは恣意的なところがない、構造的に導かれる操作なので、よく 自然な(canonical) 関数であると形容されます。

商の例を挙げると、たとえば時刻がそうです。時刻は 12 で割った余りで同一視する同値関係が入っていて、13 時と 1 時は同じものだと認識されます。また日付は、時刻を時間・分・秒を無視する同値関係で割ることで得られる商です。

Quotient.lift: 関数を商へ持ち上げる操作

α : Type 上の同値関係 r : α → α → Propr による α の商 α/r について考えます。ある型 β から商 α/r への関数を得るには、たとえば自然な関数 α → α/r と関数 β → α を合成すれば良いですが、商からの関数を得るにはどうすればいいでしょうか?

α/r の各要素は同値類であるため、α の要素の集まりです。もしも関数 f : α → β が、同値類 as : α/r のどの要素 a ∈ as に対しても同じ値を返すならば、f の定義域を α/r に持ち上げて α/r → β という関数を得ることができます。

理解しにくいと思うのでもう一度別な表現をすると、たとえば2人の人物に対して、そのひとたちの「年齢の和」を考えることはナンセンスです。これは、年齢の和が生まれた年だけによっては決まらず、「今が何年であるか」という情報にも依存してしまうからです。これが、上記で述べた「同値類のどの要素を選ぶかに依存せず同じ値を返す」という前提が成立しない状況の例になっています。逆に、「年齢の差」を考えることはできて、年齢の差を与える関数が作れます。これは、年齢の差がまさに「生年にしか依存しない」からで、生年が同じという同値類で割った商からの関数に持ち上げることができます。

この操作は Lean では Quotient.lift で実現できます。もし α : Type 上の同値関係 sr : Setoid α と関数 f : α → β が与えられていて h : ∀ x, x ≈ y → f x = f y が成り立つならば、商への持ち上げ Quotient.lift f h : Quotient sr → β が得られます。

variable {α β : Type} (sr : Setoid α)
variable (f : α → β) (h : ∀ x y, x ≈ y → f x = f y)

#check (Quotient.lift f h : Quotient sr → β)

Quotient.lift が持ち上げであると言われるのは、元の f と値が同じになるからです。つまり、f' := Quotient.lift f h としたとき f' (Quotient.mk x) = f x が成り立ちます。

variable {α β : Type} (sr : Setoid α)
variable (f : α → β) (h : ∀ x y, x ≈ y → f x = f y)

example : ∀ x, (Quotient.lift f h) (Quotient.mk sr x) = f x := by
  intro x
  rfl

Quotient.inductionOn: 同値類の代表元を取る

同値類 a : α/r は、r に関して同値な要素の集まりでした。同値類 a に対して、その中から一つ要素を選び出すことを 代表元 を取ると言います。「どれを選んでも r の意味で同じなので、どれかを取ってその同値類の代表とする」というニュアンスです。

これは Lean では Quotient.inductionOn で実現できます。これを使うと、証明の中で「同値類から代表元を取って~」というよくある議論ができます。

variable {α : Type} (sr : Setoid α)

example (a : Quotient sr) : True := by
  induction a using Quotient.inductionOn with
  | h x =>
    -- `x : α` が得られる
    guard_hyp x : α

    trivial

Quotient.sound: 同値なら商へ送って等しい

α の同値関係 sr : Setoid α による商 α/r において、x y : α が同値つまり x ≈ y であるとき、これは商へ送った時には同一視されます。つまり、言い換えれば自然な関数 Quotient.mk sr : α → α/r による像が等しくなっているはずです。

この事実には、Lean では Quotient.sound という名前が付いています。

variable {α : Type} (sr : Setoid α)
variable (x y : α) (h : x ≈ y)

/-- 同値なら商へ送って等しい -/
example : Quotient.mk sr x = Quotient.mk sr y := by
  apply Quotient.sound
  exact h

Quotient.exact: 商に送って等しいなら同値

Quotient.sound とは逆に、商に送って等しいことから同値であることを導く定理には Quotient.exact という名前がついています。

variable {α : Type} (sr : Setoid α)
variable (x y : α)

/-- 商に送って等しいなら同値 -/
example (h : Quotient.mk sr x = Quotient.mk sr y) : x ≈ y := by
  exact Quotient.exact h

使用例

順序なしペア

直積型 A × A 上の二項関係 r : A × A → A × A → Prop を、r (a₁, a₂) (b₁, b₂) := a₁ = b₁ ∧ a₂ = b₂ ∨ a₁ = b₂ ∧ a₂ = b₁ と定義します。これは、順序を無視してペアを同じと見なす同値関係です。

/-- 順序を無視してペアとして同じかどうか判定する同値関係 -/
@[instance]
def pairRel (A : Type) : Setoid (A × A) where
  r := fun p₁ p₂ => p₁.1 = p₂.1 ∧ p₁.2 = p₂.2 ∨ p₁.1 = p₂.2 ∧ p₁.2 = p₂.1
  iseqv := by constructor <;> grind

この同値関係による商を考えることで、順序なしのペア型を得ることができます。

/-- 順序なしペア -/
def UnorderedPair (A : Type) := Quotient (pairRel A)

実際、(1, 2)(2, 1)UnorderedPair への像は同じペアとして扱われます。

def sample₁ : UnorderedPair Nat := Quotient.mk _ (1, 2)
def sample₂ : UnorderedPair Nat := Quotient.mk _ (2, 1)

example : sample₁ = sample₂ := by
  apply Quotient.sound
  dsimp [(· ≈ ·), pairRel]
  grind

自然数の積の商として整数を得る

Lean の標準ライブラリの定義とは異なりますが、Int を自然数の積の商として構成することができます。

Nat × Nat を考えて、最初の要素を「正の部分」、2つめの要素を「負の部分」と考えるのです。つまり (a, b) ↦ a - b という対応を使って整数を構成します。

/-- 自然数を2つペアにしたもの。`(a, b) : PreInt` は `a - b` のつもり -/
abbrev PreInt := Nat × Nat

これは全ての整数を表すことができますが、重複があるので整数そのものになりません。この構成だとたとえば (0, 1)(1, 2) は同じ整数に対応するので、適切に同一視を入れる必要があります。(x₁, y₁) : PreInt(x₂, y₂) : PreInt が同じ整数に対応するのは x₁ - y₁ = x₂ - y₂ のときですが、これは x₁ + y₂ = x₂ + y₁ と書き直すことができます。したがって、PreInt 上の2項関係 rx₁ + y₂ = x₂ + y₁ で定義して r に関する商を取れば、整数を構成できます。

namespace PreInt
  /- ## MyIntのための同値関係の構成 -/

  /-- PreInt 上の二項関係 -/
  def rel (m n : PreInt) : Prop :=
    match m, n with
    | (m₁, m₂), (n₁, n₂) => m₁ + n₂ = m₂ + n₁

  /-- 反射律 -/
  theorem rel.refl : ∀ (m : PreInt), rel m m := by
    intro (m₁, m₂)
    dsimp [rel]
    ac_rfl

  /-- 対称律 -/
  theorem rel.symm : ∀ {m n : PreInt}, rel m n → rel n m := by
    intro (m₁, m₂) (n₁, n₂) h
    dsimp [rel] at *
    omega

  /-- 推移律 -/
  theorem rel.trans : ∀ {l m n : PreInt}, rel l m → rel m n → rel l n := by
    intro (l₁, l₂) (m₁, m₂) (n₁, n₂) hlm hmn
    dsimp [rel] at *
    omega

  /-- `PreInt.rel`は同値関係 -/
  theorem rel.equiv : Equivalence rel :=
    { refl := rel.refl, symm := rel.symm, trans := rel.trans }

  /-- `PreInt` 上の同値関係 -/
  @[instance] def srel : Setoid PreInt := ⟨rel, rel.equiv⟩

end PreInt

/-- 整数の定義 -/
def MyInt := Quotient PreInt.srel

MyInt 上にマイナス演算 (- ·) : MyInt → MyInt を定義しましょう。商からの関数は lift で構成することができます。そこでまず PreInt 上で関数を実装し、それを持ち上げる方針を取ります。

def PreInt.neg (m : PreInt) : MyInt := match m with
  | (m₁, m₂) => Quotient.mk _ (m₂, m₁)

/-- 整数上のマイナス演算 -/
def MyInt.neg : MyInt → MyInt := Quotient.lift PreInt.neg <| by
  intro (m₁, m₂) (n₁, n₂) h
  dsimp [PreInt.neg]; apply Quotient.sound
  dsimp [(· ≈ ·), PreInt.srel, PreInt.rel] at *
  omega

instance : Neg MyInt := ⟨MyInt.neg⟩

次に MyInt 上に足し算 (· + ·) : MyInt → MyInt → MyInt を定義しましょう。商からの関数なのでやはり Quotient.lift を使いたくなるのですが、引数が二つあるので Quotient.lift₂ を使う方が良いです。

def PreInt.add (m n : PreInt) : MyInt :=
  match m, n with
  | (m₁, m₂), (n₁, n₂) => Quotient.mk _ (m₁ + n₁, m₂ + n₂)

/-- 整数上の足し算 -/
def MyInt.add : MyInt → MyInt → MyInt := Quotient.lift₂ PreInt.add <| by
  intro (m₁, m₂) (n₁, n₂) (m₁', m₂') (n₁', n₂') h₁ h₂
  dsimp [PreInt.add]; apply Quotient.sound
  dsimp [(· ≈ ·), PreInt.srel, PreInt.rel] at *
  omega

instance : Add MyInt := ⟨MyInt.add⟩