Cat Girl Program Analysis β€” cgpa.isarabbithole.com
πŸ”₯ Setoid Hell β€” commiserating and coping strategies
57 replies | 3,821 views | Started by QuotientQueen | Last post: 14 minutes ago πŸ”₯ HOT πŸ“Œ PINNED TYPE THEORY
⚠️ Friendly reminder: This is a support & commiseration thread. Please share war stories, coping strategies, and code snippets. Ranting about quotient types is highly encouraged. Therapy is not provided but sympathetic meows are.
πŸ“Š THREAD POLL
What is your primary setoid coping strategy?
38% (21)
27% (15)
16% (9)
11% (6)
8% (4)

Okay, gather round fellow sufferers. I need to start this thread because I have spent the entire week trying to prove that my silly rational number type is well-defined, and I have accumulated so many Proper instances that my file is now longer than my thesis.

For those unaware: a setoid is a type paired with an explicit equivalence relation β€” it's what you're forced to use when your proof assistant doesn't have quotient types as a first-class feature. Instead of just writing β„š and having equality mean what you want, you carry around a pair (β„• Γ— β„•, β‰ˆ) forever and prove that everything respects it. Manually. For every function. Every lemma. Every single time.

-- Classic setoid pain: defining β„š as a setoid over β„• Γ— β„•
Record Setoid : Type := {
  carrier  : Type;
  equiv    : carrier β†’ carrier β†’ Prop;
  is_equiv : Equivalence equiv
}.

-- Now prove EVERY function is Proper. Yes, all of them.
Instance add_proper : Proper (rat_eq ==>
  rat_eq ==> rat_eq) rat_add.
Proof.
  (* 47 lines of arithmetic follows... *)
Qed.

I keep having to prove properness for everything. Addition. Subtraction. Multiplication. Composition. The Proper instance that says "if you apply a function to equivalent inputs you get equivalent outputs." Things that should be definitionally true. Things that are obviously true. But no, we prove them. Manually. With our tiny paws.

Share your setoid war stories below. Coping strategies also welcome. I'm told Cubical Agda has solved this. I refuse to switch on principle. (The principle is spite.)

😭 127 πŸ”₯ 89 πŸ’œ 64 🐱 41

Oh, I have a war story. I have many war stories. Strap in.

Last month I was formalizing a result about free commutative monoids in Agda (standard library setoids). The underlying type was List A quotiented by bag equality. "How hard can it be," I said. "It's just lists," I said.

-- What I thought I was doing:
BagEq : {A : Set} β†’ List A β†’ List A β†’ Set
BagEq xs ys = βˆ€ a β†’ count a xs ≑ count a ys

-- What I ended up doing for three days:
module BagSetoid where
  -- Prove reflexivity. Fine.
  -- Prove symmetry. Annoying but OK.
  -- Prove transitivity. My paws hurt.
  -- Prove append is Proper. OK.
  -- Prove map is Proper. Wait, the function also needs to be a setoid morphism?
  -- Prove map is Proper for setoid morphisms. Blood pressure rising.
  -- Prove concatMap is Proper. I need to lie down.
  -- Prove foldr is Proper. Please send help.

Three days. Three days to get a halfway-usable interface for "lists, but unordered." In HoTT with HITs this would be like, 15 lines. The FMSet HIT from the HoTT book just gives you commutativity as a path constructor and you're done.

The real killer is when you want to rewrite under a binder. Suddenly nothing works and you have to construct the entire transport manually andβ€”

😭 98 πŸ’œ 55 πŸ‘ 33

Coping strategy #1 that I've actually found useful: setoid rewriting via Setoid.rewrite / setoid_rewrite tactic in Coq, combined with pre-registering Proper instances. It doesn't fix the root problem but it makes the day-to-day pain much more bearable.

(* Register once, rewrite forever (sort of) *)
Add Parametric Morphism : my_fun
  with signature my_eq ==> my_eq ==> my_eq
  as my_fun_morphism.
Proof. (* ... *) Qed.

(* Now setoid_rewrite actually works! Sometimes! *)
Goal forall x y, my_eq x y β†’ my_eq (my_fun x x) (my_fun y y).
Proof.
  intros x y H. setoid_rewrite H. reflexivity.
Qed.

The catch is that setoid_rewrite fails silently in weird ways if the Proper instances don't perfectly match the syntactic form of your goal. See my dedicated thread about setoid_rewrite mystery failures for a catalog of suffering.

Coping strategy #2: use refinement types via { x : T | P x } when you can get away with it. Avoids the full setoid machinery if your equivalence is just "same normal form." Doesn't compose well but hey.

πŸ’‘ 77 πŸ’œ 44 🐾 28

Obligatory Cubical Agda evangelist post. I know, I know β€” but hear me out.

QuotientQueen wrote:
I'm told Cubical Agda has solved this. I refuse to switch on principle. (The principle is spite.)

The spite is valid and I respect it. But for anyone actually considering the switch: Cubical Agda gives you Higher Inductive Types natively, which means quotient types are just HITs truncated to h-level 0. Your rational numbers just become:

-- Cubical Agda: quotients that are actually quotients
data β„š : Set where
  [_]    : β„€ Γ— β„•β‚Š β†’ β„š
  eq/    : (p q : β„€ Γ— β„•β‚Š) β†’ rat-eq p q β†’ [ p ] ≑ [ q ]
  trunc  : isSet β„š

-- Functions out of β„š are forced to respect eq/ by the elimination principle.
-- No Proper instances. No manual transport. It just works. ✨

The key insight: with HITs, the proof obligation is still there β€” you have to handle the eq/ case β€” but it's structurally enforced by the type system rather than being a floating "remember to prove this for every function" social contract that you inevitably violate at 2am.

Also: I wrote a tutorial on HITs in Cubical Agda if anyone wants to dip a toe in. It's not as scary as it looks, I promise. The interval type I is your friend.

πŸŒ€ 93 πŸ’œ 71 ✨ 55 🐱 39

Okay @CubicalCatgirl, I'll engage. My objection to HIT quotients is that the eliminator is unwieldy when you want to define recursive functions. Like, to define addition on your HIT β„š you have to simultaneously prove it respects eq/ and handle the trunc case. The trunc case in particular:

-- Cubical Agda β„š-elim when you just want to add two rationals:
β„š-elim : βˆ€ {β„“} (B : β„š β†’ hSet β„“)
  β†’ (f : (p : β„€ Γ— β„•β‚Š) β†’ B [ p ])
  β†’ (βˆ€ p q (r : rat-eq p q) β†’ PathP (Ξ» i β†’ B (eq/ p q r i)) (f p) (f q))
  β†’ (x : β„š) β†’ B x

-- You get this for free if B is a set (trunc case is trivial).
-- BUT. If B is not obviously a set, you cry.

The good news is if your target type is already a set (like β„š itself!), the trunc case is trivial and you just pattern match away. I'll… admit this is better than what I'm doing. But I have committed to setoid pain and I will not be swayed by good arguments.

Someone mentioned QITs vs HITs earlier β€” what's the take on Quotient Inductive-Inductive Types for this? I've seen the Altenkirch & Kaposi paper but haven't dug in.

🀣 82 πŸ’œ 48 🐱 37

Re: QITs β€” they're essentially HITs truncated to be sets (h-sets). The difference from plain quotient types is that you can have infinitely many constructors specified simultaneously with their equality conditions. This is super useful for defining things like:

  • Cauchy reals (as a QIT, not a setoid β€” no more carrying β„• β†’ β„š everywhere)
  • Type-theoretic syntax / intrinsically-typed terms with substitution laws built in
  • Free algebraic structures with their equations
-- QIT example: integers as a quotient of (β„• Γ— β„•, diff-eq)
-- Instead of setoid: (β„• Γ— β„•, Ξ» (a,b) (c,d) β†’ a+d = b+c)

data β„€-QIT : Set where
  diff   : β„• β†’ β„• β†’ β„€-QIT           -- n - m
  cancel : βˆ€ n m k β†’
    diff (n + k) (m + k) ≑ diff n m  -- (n+k)-(m+k) = n-m
  isSet  : isSet β„€-QIT

-- The cancel constructor IS the setoid relation, baked into the type.
-- No external β‰ˆ to carry. No Proper instances. Beauty.

The isSet constructor (from the HoTT book Ch. 6) truncates the HIT to a set so you don't have to worry about higher paths. This is what distinguishes QITs from general HITs.

See the Altenkirch-Kaposi QIIT paper for the general theory. It's a bit dense but worth it. We have a reading group thread here too.

πŸ’‘ 64 πŸ’œ 52 🧊 29

Hot take: the true villain of setoid hell isn't the setoids themselves. It's the lack of a quotient type eliminator that the compiler verifies. With setoids, the invariant "all functions must respect β‰ˆ" is a social contract enforced by shame, not a type-level guarantee.

I once spent 6 hours debugging a proof that should have been true before realizing I had defined a function that didn't actually respect my equivalence relation. The type checker was fine with it! Because of course it was β€” from the type checker's perspective, I just defined a function carrier β†’ carrier that happened to use the wrong element of an equivalence class.

A wise catgirl once said:
"Setoids are not the problem. The problem is that nothing stops you from leaving the setoid without noticing."

This is also why the Lean 4 approach of Quot.lift + Quot.sound as primitives is so much nicer. You literally cannot define a function on a quotient without proving it well-defined at definition site. No forgetting, no 2am bugs. Discuss: is this worth the other limitations of Lean's quotient axiom approach?

See also: Lean 4 Quotient Primitives β€” a love letter

πŸ”₯ 91 πŸ’― 67 πŸ’œ 44

@RefinementRawr this is the correct hot take and I will die on this hill.

The classic Leanprover discussion on this (Buzzard's comparison) essentially says that the ergonomic difference between Lean's primitive quotient and Coq's setoid hell is enormous in practice when building a mathematical library at scale. The more abstraction layers you add, the worse it compounds.

The Coq side of the argument is that Lean's quotient axiom (propositional extensionality + quotient) is non-computational β€” you lose the ability to extract programs that compute by reduction. Coq's setoids are painful but at least reduction is still definitional.

Comparison table because I love a comparison table:

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Feature          β”‚ Coq Setoids    β”‚ Lean 4 Quot.   β”‚ Cubical Agda     β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ First-class quot β”‚      βœ—         β”‚      βœ“         β”‚       βœ“ (HITs)   β”‚
β”‚ Definitional =   β”‚      βœ“         β”‚      βœ—         β”‚       βœ“          β”‚
β”‚ Computable extr. β”‚      βœ“         β”‚  Partial       β”‚       βœ“          β”‚
β”‚ Univalence       β”‚   Axiom        β”‚   Axiom        β”‚    Built-in βœ“    β”‚
β”‚ Pain level       β”‚    MAX πŸ”₯      β”‚   Moderate     β”‚   Medium-high    β”‚
β”‚ Cat-girlness     β”‚    High        β”‚   Moderate     β”‚    MAXIMUM 🐱    β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Linking the full proof assistant comparison thread for context. Also: Univalence as an axiom vs. cubical path types is relevant background.

πŸ“Š 88 πŸŒ€ 74 πŸ’œ 61 🐱 55
βœ— Post #9 deleted by moderator β€” [off-topic: arguing that ZFC is better than type theory] β€” RulesRules Β§ 3.2b

Update from the trenches: I've discovered a new circle of setoid hell I didn't know existed. Setoids over setoids.

I needed a quotient of a quotient. So: a setoid whose carrier is already a setoid. You might think "just compose the equivalence relations." Ha. Ha ha. The carrier of a setoid is a type, and to use a setoid as a carrier you need to… pass the equivalence along at every point and prove everything respects both relations.

-- Setoid morphisms: the correct notion of "function" between setoids
Record SetoidMorphism (A B : Setoid) : Type := {
  map      : carrier A β†’ carrier B;
  respects : βˆ€ x y, equiv A x y β†’ equiv B (map x) (map y)
}.

-- Now I need morphisms between morphisms. A setoid of morphisms.
-- The equivalence: two morphisms are β‰ˆ if they agree pointwise up to β‰ˆ_B
-- To prove this is an equivalence: reflexivity, symmetry, transitivity... again.
-- We're going deeper.

I've decided to name the layers. Layer 0: regular setoid. Layer 1: setoid of setoid morphisms. Layer 2: setoid of setoid morphisms between morphism setoids. I am on Layer 3 and I can no longer see the surface.

Is there a name for this phenomenon? I want to formally register my suffering.

😱 103 πŸ’€ 79 😭 68 🐱 41

@QuotientQueen what you've discovered is the category of setoids, which is actually a perfectly fine and well-studied mathematical object! Setoids and setoid morphisms form a category (technically a locally small one). The name for what you're in is:

✨ Setoid Hell, Layer Ο‰ ✨

More precisely: you're building the internal category of setoids within type theory, which requires exactly this tower of morphism-setoids. This is actually the mathematical foundation of Bishop-style constructive mathematics. It's not a bug, it's the intended experience.

The "is there a better way" answer is: yes, and it's called an exact completion. You can formally "complete" a category with the setoid construction to get a category with quotients. But doing this inside a proof assistant without native quotients is left as an exercise to the suffering reader.

See: Bishop-style constructivism in proof assistants and exact completions in type theory.

πŸŽ“ 56 πŸ’œ 44 πŸ€“ 38
↩ Post a Reply