Zulip Chat Archive

Stream: new members

Topic: Pattern matching on quotient types


Brandon Harad (Mar 15 2024 at 17:44):

I am trying to define the integers with a quotient (of pairs of naturals) instead of with an inductive type as an exercise. I would like to make a function which pattern matches inputs based on my defined constants and then proceeds recursively. For example, consider the following function on my quotient type Z:

def mult2 : Z  Z
  | 0 => 0
  | z + 1 => 2 + mult2 z
  | z - 1 => -2 + mult2 z

I have already implemented addition, ofNat, etc in a sensible manner. How can I do this?

Kevin Buzzard (Mar 15 2024 at 19:27):

You'll have to ask someone more CS-oriented about how to get the notation working (if indeed it's even possible) but there's nothing stopping you defining MyInt.pos_neg_rec or whatever you want to call it, which eats a type X, a term x0 : X and two functions Nat x X -> X, and spits out a function Int -> X satisfying the identities you want; you can then define functions recursively using this.

Kevin Buzzard (Mar 15 2024 at 19:33):

import Mathlib

abbrev MyInt := 

universe u -- we want to allow `Prop` and `Type` so we can do induction and recursion.
-- So we let the "motive" of the recursor take values in `Sort u`, because `Sort 0 = Prop`
-- and `Sort 1 = Type`.

def MyInt.pos_neg_rec_on {M : MyInt  Sort u} (m0 : M 0) (mpos :  (n : ), M n  M (n + 1))
  (mneg :  n : , M (-↑n)  M (-↑n - 1)) (z : MyInt) : M z := sorry -- hard exercise!

-- specification

variable {M : MyInt  Sort u} (m0 : M 0) (mpos :  (n : ), M n  M (n + 1))
  (mneg :  n : , M (-↑n)  M (-↑n - 1))

lemma pos_neg_rec_on_zero : MyInt.pos_neg_rec_on m0 mpos mneg 0 = m0 := sorry

lemma pos_neg_rec_on_pos (n : ) :
  MyInt.pos_neg_rec_on m0 mpos mneg (n + 1) = mpos n (MyInt.pos_neg_rec_on m0 mpos mneg n) := sorry

lemma pos_neg_rec_on_neg (n : ) :
  MyInt.pos_neg_rec_on m0 mpos mneg (-↑n - 1) = mneg n (MyInt.pos_neg_rec_on m0 mpos mneg (-↑n)) := sorry

Chris Bailey (Mar 15 2024 at 19:34):

There is a match_pattern attribute, I don't remember exactly what the restrictions and capabilities are. The top answer here looks reasonable: https://proofassistants.stackexchange.com/questions/2438/why-can-addition-be-used-in-pattern-matching-nats-but-not-multiplication

I don't recall them using pattern matching, but this definition of the integers is a section in Logical Verification (chapter 12) if you're interested in seeing how others did it at some point (https://github.com/blanchette/logical_verification_2023/tree/main).

Kevin Buzzard (Mar 15 2024 at 19:36):

I guess the way to define this in your situation is first to define an analogous function on Nat x Nat and then prove it descends (or "lifts", as the computer scientists call it) to Int. These things are quite tricky :-)

Kevin Buzzard (Mar 15 2024 at 19:38):

I don't think they make the recursor in LV though (and neither do I in my treatment here ).

Kyle Miller (Mar 16 2024 at 02:23):

@Chris Bailey match_pattern won't work here -- a way to think about that attribute is that it could also be called pattern_inline. In matches, definitions with this attribute get inlined, and it has to be a valid pattern involving constructors of inductive types in th eend.

Kyle Miller (Mar 16 2024 at 03:52):

@Brandon Harad A lot of times it's better to define something as a quotient by not using Quot, but instead by realizing there is a normal form. Then, you define the Quot API for this. The point is that quotients should be thought of in terms of their universal properties, so you don't need to literally work with Quot itself.

This doesn't help with your match question (I'm not sure there's a way to do that), but I thought I'd mention it.

For example, rather than a quotient on pairs, you can define a subtype on pairs:

def Z := {p : Nat × Nat // p.1 = 0  p.2 = 0}

And then the constructor can be defined using normalization:

def Z.mk : Nat  Nat  Z
  | a, 0 => ⟨(a, 0), Or.inr rfl
  | 0, b => ⟨(0, b), Or.inl rfl
  | a+1, b+1 => Z.mk a b

theorem Z.mk_zero_snd (a : Nat) : Z.mk a 0 = ⟨(a, 0), Or.inr rfl := by cases a <;> rfl
theorem Z.mk_zero_fst (b : Nat) : Z.mk 0 b = ⟨(0, b), Or.inl rfl := by cases b <;> rfl

@[elab_as_elim]
theorem Z.ind {β : Z  Prop} (mk :  (a b : Nat), β (Z.mk a b)) (q : Z) : β q := by
  obtain ⟨⟨a, b⟩, h | h := q <;> dsimp at h <;> subst_vars
  · simpa [Z.mk_zero_fst] using mk 0 b
  · simpa [Z.mk_zero_snd] using mk a 0

def Z.lift {β : Sort _} (f : Nat  Nat  β)
    (_ :  (a b a' b' : Nat), a + b' = a' + b  f a b = f a' b')
    (n : Z) : β :=
  f n.1.1 n.1.2

theorem Z.mk_eq_of_ge {a b : Nat} (h : a  b) : Z.mk a b = ⟨(a - b, 0), Or.inr rfl := by
  induction a generalizing b
  · simp at h; subst_vars; rfl
  next n ih =>
    cases b
    · rw [Z.mk_zero_snd]; rfl
    · rw [Z.mk, ih]; simp; omega

theorem Z.mk_eq_of_le {a b : Nat} (h : a  b) : Z.mk a b = ⟨(0, b - a), Or.inl rfl := by
  induction b generalizing a
  · simp at h; subst_vars; rfl
  next n ih =>
    cases a
    · rw [Z.mk_zero_fst]; rfl
    · rw [Z.mk, ih]; simp; omega

@[simp]
theorem Z.lift_mk {β : Sort _} (f : Nat  Nat  β)
    (h :  (a b a' b' : Nat), a + b' = a' + b  f a b = f a' b')
    (a b : Nat) : Z.lift f h (Z.mk a b) = f a b := by
  unfold Z.lift
  obtain hab|hab := le_or_gt a b
  · rw [Z.mk_eq_of_le hab, h]
    dsimp
    omega
  · rw [Z.mk_eq_of_ge hab.le, h]
    dsimp
    omega

theorem Z.sound (a b a' b' : Nat) (h : a + b' = a' + b) : Z.mk a b = Z.mk a' b' := by
  obtain hab|hab := le_or_gt a b <;>
  obtain hab'|hab' := le_or_gt a' b' <;>
  · apply Subtype.ext
    simp [Z.mk_eq_of_le, Z.mk_eq_of_ge, hab, hab', LT.lt.le]
    omega

For example, here's how you could define addition using this API:

def Z.add (x y : Z) : Z :=
  x.lift (fun a b => y.lift (fun c d => Z.mk (a + c) (b + d)) <| by
    intros a b c d h
    dsimp
    apply Z.sound
    omega) <| by
  intros a b c d h
  dsimp
  congr! 3
  apply Z.sound
  omega

theorem Z.add_mk (a b c d : Nat) :
    Z.add (Z.mk a b) (Z.mk c d) = Z.mk (a + c) (b + d) := by
  simp [Z.add]

Kevin Buzzard (Mar 16 2024 at 10:18):

@Kyle Miller did you just tag the wrong Brando(n)? (If so, note that at least a few years ago, editing your post doesn't tag the new person)

Kevin Buzzard (Mar 16 2024 at 10:23):

Note that the disadvantage of the subtype definition is that the API for the algebra structure on the integers becomes more fiddly to write (you're constantly having to break into cases when defining and proving). But it's a one time cost.

Eric Wieser (Mar 16 2024 at 12:04):

Kevin Buzzard said:

... there's nothing stopping you defining MyInt.pos_neg_rec or whatever you want to call it, ...

When you do this, you should tag it @[elab_as_elim] to make it a little easier to use

Kyle Miller (Mar 16 2024 at 14:43):

(Oops, yes, I tagged the wrong Brandon. Edited it.)

Kyle Miller (Mar 16 2024 at 14:44):

@Kevin Buzzard With what I wrote, now you never have to open up the definition ever again. You use the universal property of the quotient.

Kyle Miller (Mar 16 2024 at 15:01):

I extended the code in the message, and it includes this example of how to then define addition without breaking Z open:

def Z.add (x y : Z) : Z :=
  x.lift (fun a b => y.lift (fun c d => Z.mk (a + c) (b + d)) <| by
    intros a b c d h
    dsimp
    apply Z.sound
    omega) <| by
  intros a b c d h
  dsimp
  congr! 3
  apply Z.sound
  omega

theorem Z.add_mk (a b c d : Nat) :
    Z.add (Z.mk a b) (Z.mk c d) = Z.mk (a + c) (b + d) := by
  simp [Z.add]

Kyle Miller (Mar 16 2024 at 15:04):

The theorem Z.sound is sort of "responsible" for doing the case analysis you're talking about. That's now wrapped up in a theorem though, so you never have to do it again.

Brandon Harad (Mar 16 2024 at 23:54):

Interesting implementation. I hadn't thought of that. Thank you all.

Eric Wieser (Mar 17 2024 at 00:16):

One small advantage of working with literal quotients instead of a normal form is that they allow lift_mk to be true by definition

Eric Wieser (Mar 17 2024 at 00:17):

Perhaps there should be something like csimp for associating a normal form to a quotient, as the normal form is more efficient for actual computation

Kyle Miller (Mar 17 2024 at 00:47):

Well, you could always write an identity function that normalizes the underlying term of a quotient, and then use @[csimp] lemmas to swap the operations out for ones that insert this normalizer.

If you don't combine normalization with operations, I'm not sure where the compiler would insert normalization.

Or maybe you mean it could be @[csimp] for quotient types themselves? Where you can tell the compiler to use another type that implements the whole interface and universal property of being a quotient?


Last updated: May 02 2025 at 03:31 UTC