Zulip Chat Archive
Stream: lean4
Topic: Puzzle: Decidable HEq
Joachim Breitner (Aug 29 2025 at 09:39):
Idle experimentation; I was exploring types for lean fails to derive DecidableEq and noticed that it cannot handle types with indices where the constructor parameters are not determined by the indices. I was wondering if this is a fundamental issue, or if it would be possible to generalize to DecidableHEq and then make it go through, but I couldn't quite close the goal. MWE might be
opaque f : Nat → Nat
inductive I : (n : Nat) → Type where
| mk n : I (f n)
def instDecidableHEqI {n m} (x : I n) (y : I m) : Decidable (HEq x y) :=
match x, y with
| I.mk n1, I.mk n2 =>
if h : n1 = n2 then
by subst h; exact isTrue HEq.rfl
else
isFalse <| by
sorry
Can I make progress here, or is this doomed?
Robin Arnez (Aug 29 2025 at 10:46):
Nah, you can't, there's a counterexample:
def f : Nat → Nat := fun x => x % 2
def I (n : Nat) : Type :=
match n with
| 0 => Nat
| 1 => Nat
| _ + 2 => Empty
def I.mk0 (n : Nat) : I 0 := n
def I.mk1 (n : Nat) : I 1 := n
def I.val0 (n : I 0) : Nat := n
def I.val1 (n : I 1) : Nat := n
def I.mk (n : Nat) : I (f n) :=
match hf : f n with
| 0 => n / 2
| 1 => n / 2
| _ + 2 => False.elim (by grind [f])
@[elab_as_elim, induction_eliminator]
def I.rec.{u_1} {motive : (n : Nat) → (t : I n) → Sort u_1}
(mk : ∀ n, motive (f n) (I.mk n)) {n : Nat} (t : I n) :
motive n t :=
match n with
| 0 => cast ?_ (mk ((show Nat from t) * 2))
| 1 => cast ?_ (mk ((show Nat from t) * 2 + 1))
| _ + 2 => t.elim
where finally
· simp only [f, I.mk, Nat.succ_eq_add_one]
congr
· simp
· refine HEq.trans (I.mk.match_1.congr_eq_1 (heq_1 := Nat.mul_mod_left t 2) ..) ?_
rw [Nat.mul_div_cancel _ (by decide)]
· simp only [f, I.mk, Nat.succ_eq_add_one]
congr
· simp
· refine HEq.trans (I.mk.match_1.congr_eq_2 (heq_1 := ?_) ..) ?_
· simp
· grind
theorem I.rec_mk {motive : (n : Nat) → (t : I n) → Sort u_1}
(mk : ∀ n, motive (f n) (I.mk n)) (n : Nat) :
I.rec (motive := motive) mk (I.mk n) = mk n := by
simp only [I.mk, Nat.succ_eq_add_one, rec, f]
rcases Nat.mod_two_eq_zero_or_one n with hn | hn <;> grind
example : ∃ (n1 n2 : Nat), I.mk n1 ≍ I.mk n2 ∧ n1 ≠ n2 := by
exists 0, 1
Damiano Testa (Aug 29 2025 at 10:53):
Is this really a counterexample, or is it simply showing that you cannot deduce isFalse when n1 and n2 are different?
Joachim Breitner (Aug 29 2025 at 10:55):
That's not my inductive I :-).
Robin Arnez (Aug 29 2025 at 10:56):
Well, it is a counterexample in the sense that it tells you that there is a model of Lean where I is defined in such a way that the isFalse branch is false
Arthur Adjedj (Aug 29 2025 at 10:58):
Joachim Breitner said:
Can I make progress here, or is this doomed?
Because type constructors are (in most cases) not injective, it is in general not possible to prove that two constructor of an inductive family with distinct indices are not equal, e.g for the following type, ¬ foo ≍ bar is not provable.
inductive Foo : Nat → Type
| foo : Foo 1
| bar : Foo 2
Arthur Adjedj (Aug 29 2025 at 10:59):
This issue also appears in your case, since I.mk n1 and I.mk n2 have distinct types/indices. I would say that this is probably doomed.
Robin Arnez (Aug 29 2025 at 11:29):
I will mention though that I actually tried something similar some time ago, but with the necessary equalities:
opaque f : Nat → Nat
inductive I : (n : Nat) → Type where
| mk n : I (f n)
@[elab_as_elim]
abbrev HEq.recEq.{u_1, u} {α : Sort u} {a : α} {motive : (b : α) → (t : a ≍ b) → Sort u_1}
(refl : motive a (HEq.refl a)) {b : α} (t : a ≍ b) : motive b t :=
@Eq.rec α a (fun b t => motive b (heq_of_eq t)) refl b (eq_of_heq t)
def I.noConfusionHEqType (P : Sort u) (a : I m) (b : I n) : Sort u :=
a.casesOn fun n1 => b.casesOn fun n2 => (n1 = n2 → P) → P
def I.noConfusionHEq {P : Sort u} {m : Nat} {a : I m} :
{n : Nat} → (h₁ : m = n) → {b : I n} → (h₂ : a ≍ b) → I.noConfusionHEqType P a b :=
Eq.rec (HEq.recEq (a.casesOn fun _ h => h rfl))
def I.decHEq {n m} (hnm : n = m) (x : I n) (y : I m) : Decidable (HEq x y) :=
match x, y with
| I.mk n1, I.mk n2 =>
if h : n1 = n2 then
by subst h; exact isTrue HEq.rfl
else
isFalse (fun heq => I.noConfusionHEq hnm heq h)
instance : DecidableEq (I n) := fun _ _ =>
haveI := I.decHEq (rfl : n = n)
decidable_of_decidable_of_iff ⟨eq_of_heq, heq_of_eq⟩
Joachim Breitner (Aug 29 2025 at 11:42):
Robin Arnez schrieb:
Well, it is a counterexample in the sense that it tells you that there is a model of Lean where
Iis defined in such a way that theisFalsebranch is false
Ah, ok, I was too quick to complain. You define constructors and a recursor, so if it would work for the normal indictive it would work for yours as well. Thanks!
(Still a bit surprising if I think of it as just data)
Robin Arnez (Aug 29 2025 at 11:47):
I just realized that I did it way more complicated than it had to be:
def f : Nat → Nat := id
def I (n : Nat) := Unit
def I.mk (n : Nat) : I n := ()
def I.rec {motive : (n : Nat) → (t : I n) → Sort u_1}
(mk : ∀ n, motive n (I.mk n)) {n : Nat} (t : I n) : motive n t := mk n
theorem I.rec_mk {motive : (n : Nat) → (t : I n) → Sort u_1}
(mk : ∀ n, motive n (I.mk n)) (n : Nat) : I.rec (motive := motive) mk (I.mk n) = mk n := rfl
Kyle Miller (Aug 29 2025 at 14:41):
When I've run into this issue before (decidable equality for inductive types with complicated indices), one trick is to first create an auxiliary decidable equality function that also takes in index equality proofs, then call it with rfl in the instance. The index equalities are passed to the match to generalize them; using rfl for the pattern gets match to do dependent elimination (if that works), and otherwise you can either simulate dependent elimination yourself, if you happen to know some extra facts about the type, or you can define some projection functions that give the constructor fields, which somehow helps as a technique to prove that the field disequality gives a contradiction, because it enables a congruence argument.
I think the general idea is you imagine defining a type I' that doesn't have indices, define a function from I to I' that forgets indices, define decidable equality on I', then pull it back to I. You lose the sub-quadratic case analysis this way, but possibly you can recover it by using constructor indices somehow (indices in the nth constructor sense).
import Mathlib.Tactic
opaque f : Nat → Nat
inductive I : (n : Nat) → Type where
| mk n : I (f n)
def I.decEqAux {n m} (x : I n) (y : I m) (h1 : n = m) :
Decidable (x ≍ y) :=
match x, y, h1 with
| I.mk n1, I.mk n2, h1 =>
if h : n1 = n2 then
isTrue <| by subst h; exact HEq.rfl
else
isFalse <| by
intro h'
let getVal : {n : Nat} → (x : I n) → Nat := fun x => match x with | .mk n => n
have : getVal (mk n1) = getVal (mk n2) := by
refine congr_heq ?_ h'
exact congr_arg_heq _ h1
exact h this
instance {n} : DecidableEq (I n) := fun x y =>
@decidable_of_iff _ (x ≍ y) heq_iff_eq (I.decEqAux x y rfl)
Kyle Miller (Aug 29 2025 at 14:53):
Here's a more complicated example with multiple constructors, to show the general design pattern. The ctorIdx does end up working as a way to limit it to a linear number of cases.
import Mathlib.Tactic
opaque f : Nat → Nat
inductive I : (n : Nat) → Type where
| mk n : I (f n)
| mk' (a : Nat) (z : I a) : I (2 * a)
def I.ctorIdx {n} : I n → Nat
| .mk .. => 0
| .mk' .. => 1
def I.mk_n {n} (x : I n) (h : x.ctorIdx = 0) : Nat :=
match x, h with
| .mk n, _ => n
def I.mk'_a {n} (x : I n) (h : x.ctorIdx = 1) : Nat :=
match x, h with
| .mk' a _, _ => a
def I.mk'_z {n} (x : I n) (h : x.ctorIdx = 1) : I (I.mk'_a x h) :=
match x, h with
| .mk' _ z, _ => z
def I.decEqAux {n m} (x : I n) (y : I m) (h1 : n = m) :
Decidable (x ≍ y) :=
if hidx : x.ctorIdx = y.ctorIdx then
match x, y, hidx, h1 with
| I.mk n1, I.mk n2, _, h1 =>
if h : n1 = n2 then
isTrue <| by subst h; exact HEq.rfl
else
isFalse <| by
intro h'
have : (mk n1).mk_n rfl = (mk n2).mk_n rfl := by
congr
exact h this
| I.mk' a1 z1, I.mk' a2 z2, _, h1 =>
if ha : a1 = a2 then
match I.decEqAux z1 z2 ha with
| isTrue hz => isTrue <| by subst_vars; rfl
| isFalse hz => isFalse <| by
intro h'
have : (mk' a1 z1).mk'_z rfl ≍ (mk' a2 z2).mk'_z rfl := by
congr
exact hz this
else
isFalse <| by
intro h'
have : (mk' a1 z1).mk'_a rfl = (mk' a2 z2).mk'_a rfl := by
congr
exact ha this
else
isFalse <| by
intro h
subst_vars
contradiction
instance {n} : DecidableEq (I n) := fun x y =>
@decidable_of_iff _ (x ≍ y) heq_iff_eq (I.decEqAux x y rfl)
Kyle Miller (Aug 29 2025 at 15:03):
The IR for this is reasonable too.
def I.decEqAux._redArg (x_1 : @& obj) (x_2 : @& obj) : u8 :=
let x_3 : tobj := I.ctorIdx._redArg x_1;
let x_4 : tobj := I.ctorIdx._redArg x_2;
let x_5 : u8 := Nat.decEq x_3 x_4;
dec x_4;
dec x_3;
case x_5 : u8 of
Bool.false →
ret x_5
Bool.true →
case x_1 : obj of
I.mk →
let x_6 : tobj := proj[0] x_1;
let x_7 : tobj := proj[0] x_2;
let x_8 : u8 := Nat.decEq x_6 x_7;
ret x_8
I.mk' →
let x_9 : tobj := proj[0] x_1;
let x_10 : obj := proj[1] x_1;
let x_11 : tobj := proj[0] x_2;
let x_12 : obj := proj[1] x_2;
let x_13 : u8 := Nat.decEq x_9 x_11;
case x_13 : u8 of
Bool.false →
ret x_13
Bool.true →
let x_14 : u8 := I.decEqAux._redArg x_10 x_12;
ret x_14
Joachim Breitner (Aug 29 2025 at 15:37):
That’s clever! It’s not an itch that I feel like I need to scratch at the moment, but I’ll try to keep it in mind if we ever have user-demand for deriving DecidableEq for such types.
Aaron Liu (Sep 08 2025 at 01:12):
I recently had to do the decidable heq trick on some of my types
Aaron Liu (Sep 08 2025 at 01:12):
there was one really annoying case
Last updated: Dec 20 2025 at 21:32 UTC