Zulip Chat Archive
Stream: Is there code for X?
Topic: mutual dependent injectivity
Aaron Liu (Oct 30 2025 at 10:17):
I have some mutual inductive types in my code, and I've defined some dependent functions with them, which are obviously injective. But proving they are injective was weirdly annoying and took ~50 lines. I'm wondering if there's an easier way.
universe u v
namespace Ex
inductive Object (ι : Type u) : Type u where
| of (i : ι) : Object ι
| unit : Object ι
| prod (left right : Object ι) : Object ι
| hom (source target : Object ι) : Object ι
mutual
inductive Normal {ι : Type u} {κ : Type v} (ζ : κ → Object ι) :
(ctx : List (Object ι)) → (typ : Object ι) → Type (max u v) where
| ofNeutral {ctx : List (Object ι)} {t : ι} (n : Neutral ζ ctx (.of t)) : Normal ζ ctx (.of t)
| lam {ctx : List (Object ι)} (dom : Object ι) {tb : Object ι}
(body : Normal ζ (dom :: ctx) tb) : Normal ζ ctx (.hom dom tb)
| unit (ctx : List (Object ι)) : Normal ζ ctx .unit
| prod {ctx : List (Object ι)} {tl tr : Object ι}
(left : Normal ζ ctx tl) (right : Normal ζ ctx tr) : Normal ζ ctx (.prod tl tr)
inductive Neutral {ι : Type u} {κ : Type v} (ζ : κ → Object ι) :
(ctx : List (Object ι)) → (typ : Object ι) → Type (max u v) where
| of (k : κ) (ctx : List (Object ι)) : Neutral ζ ctx (ζ k)
| app {ctx : List (Object ι)} {typed typea : Object ι}
(fn : Neutral ζ ctx (.hom typed typea)) (arg : Normal ζ ctx typed) : Neutral ζ ctx typea
| left {ctx : List (Object ι)} {tl tr : Object ι} (tup : Neutral ζ ctx (.prod tl tr)) :
Neutral ζ ctx tl
| right {ctx : List (Object ι)} {tl tr : Object ι} (tup : Neutral ζ ctx (.prod tl tr)) :
Neutral ζ ctx tr
| bvar {ctx : List (Object ι)} (n : Nat) (typ : Object ι) (sat : typ ∈ ctx[n]?) :
Neutral ζ ctx typ
end
mutual
inductive Normalu {ι : Type u} {κ : Type v} (ζ : κ → Object ι) :
(ctx : List (Object ι)) → (typ : Object ι) → Type (max u v) where
| ofNeutral {ctx : List (Object ι)} {t : ι} (n : Neutralu ζ ctx (.of t)) : Normalu ζ ctx (.of t)
| lam {ctx : List (Object ι)} (dom : Object ι) {tb : Object ι}
(body : Normalu ζ (dom :: ctx) tb) : Normalu ζ ctx (.hom dom tb)
inductive Neutralu {ι : Type u} {κ : Type v} (ζ : κ → Object ι) :
(ctx : List (Object ι)) → (typ : Object ι) → Type (max u v) where
| of (k : κ) (ctx : List (Object ι)) : Neutralu ζ ctx (ζ k)
| app {ctx : List (Object ι)} {typed typea : Object ι}
(fn : Neutralu ζ ctx (.hom typed typea)) (arg : Normalu ζ ctx typed) : Neutralu ζ ctx typea
| bvar {ctx : List (Object ι)} (n : Nat) (typ : Object ι) (sat : typ ∈ ctx[n]?) :
Neutralu ζ ctx typ
end
mutual
def Normalu.toNormal {ι : Type u} {κ : Type v} {ζ : κ → Object ι}
{ctx : List (Object ι)} {typ : Object ι} (t : Normalu ζ ctx typ) : Normal ζ ctx typ :=
match t with
| .ofNeutral n => .ofNeutral n.toNeutral
| .lam dom body => .lam dom body.toNormal
def Neutralu.toNeutral {ι : Type u} {κ : Type v} {ζ : κ → Object ι}
{ctx : List (Object ι)} {typ : Object ι} (t : Neutralu ζ ctx typ) : Neutral ζ ctx typ :=
match t with
| .of k ctx => .of k ctx
| .app fn arg => .app fn.toNeutral arg.toNormal
| .bvar n typ sat => .bvar n typ sat
end
-- want to prove: `Normalu.toNormal` and `Neutralu.toNeutral` are both `Function.Injective`
Joachim Breitner (Oct 30 2025 at 12:14):
What have you tried (induction, recursive theorems)? Why was it hard?
Joachim Breitner (Oct 30 2025 at 12:15):
And I assume the example is simplified? These functions look like the identity to me (at least on the phone )
Kenny Lau (Oct 30 2025 at 12:18):
Joachim Breitner said:
look like the identity
it's from xxxu to xxx, and xxxu seems to be simplified versions of xxx (with less constructors)
Floris van Doorn (Oct 30 2025 at 12:55):
You need that ζ is injective to prove this, right?
Floris van Doorn (Oct 30 2025 at 13:01):
Here is some code that works and is significantly less than 50 lines, but is very ugly:
attribute [simp] Normalu.toNormal Neutralu.toNeutral
open Function
mutual
theorem toNormal_inj {ι : Type u} {κ : Type v} {ζ : κ → Object ι}
{ctx : List (Object ι)} {typ : Object ι} (hζ : Injective ζ) :
Injective (Normalu.toNormal : Normalu ζ ctx typ → _)
| .ofNeutral .., .ofNeutral .., hxy => by simp_all; exact toNeutral_inj hζ hxy
| .lam .., .lam .., hxy => by simp_all; exact toNormal_inj hζ hxy
theorem toNeutral_inj {ι : Type u} {κ : Type v} {ζ : κ → Object ι}
{ctx : List (Object ι)} {typ : Object ι} (hζ : Injective ζ) :
Injective (Neutralu.toNeutral : Neutralu ζ ctx typ → _)
| x, y, hxy => by
cases x <;> try cases y <;> simp_all
· rename_i k
have : ∀ (u : Object ι) (h : u = ζ k) (y : Neutralu ζ ctx u),
(Neutralu.of k ctx).toNeutral = (h ▸ y).toNeutral → Neutralu.of k ctx = h ▸ y := by
intro u h y hy
cases y <;> try cases h <;> simp_all
cases hζ h
cases h
simp_all
specialize this _ rfl y
simp_all
· cases hxy.1
simp at *
exact ⟨toNeutral_inj hζ hxy.1, toNormal_inj hζ hxy.2⟩
end
Floris van Doorn (Oct 30 2025 at 13:04):
(I'm not sure why Lean managed to figure out termination of this proof, but not of the proof where I did to_Normal_inj also in tactic mode. I expected I had to do both with pattern matching)
Joachim Breitner (Oct 30 2025 at 13:04):
Interesting that
theorem toNormal_inj {ι : Type u} {κ : Type v} {ζ : κ → Object ι}
{ctx : List (Object ι)} {typ : Object ι} (hζ : Injective ζ) :
Injective (Normalu.toNormal : Normalu ζ ctx typ → _)
| .ofNeutral .., .ofNeutral .., hxy => by simp_all; exact toNeutral_inj hζ hxy
| .lam .., .lam .., hxy => by simp_all; exact toNormal_inj hζ hxy
works but not
| .lam dom1 body₁, .lam dom2 body₂, hxy => by simp_all; exact toNormal_inj hζ hxy
does not. Ah, but
| .lam dom body₁, .lam .(dom) body₂, hxy => by simp_all; exact toNormal_inj hζ hxy
does! :bulb:
Floris van Doorn (Oct 30 2025 at 13:29):
Here a bit cleaner with pattern matching for both lemmas:
attribute [simp] Normalu.toNormal Neutralu.toNeutral
open Function
mutual
theorem toNormal_inj {ι : Type u} {κ : Type v} {ζ : κ → Object ι}
{ctx : List (Object ι)} {typ : Object ι} (hζ : Injective ζ) :
Injective (Normalu.toNormal : Normalu ζ ctx typ → _)
| .ofNeutral .., .ofNeutral .., hxy => by simp_all; exact toNeutral_inj hζ hxy
| .lam .., .lam .., hxy => by simp_all; exact toNormal_inj hζ hxy
theorem toNeutral_inj {ι : Type u} {κ : Type v} {ζ : κ → Object ι}
{ctx : List (Object ι)} {typ : Object ι} (hζ : Injective ζ) :
Injective (Neutralu.toNeutral : Neutralu ζ ctx typ → _)
| .of k .., y, hxy => by
have : ∀ (u : Object ι) (h : u = ζ k) (y : Neutralu ζ ctx u),
(Neutralu.of k ctx).toNeutral = (h ▸ y).toNeutral → Neutralu.of k ctx = h ▸ y := by
intro u h y hy
cases y
· cases hζ h
simp_all
all_goals { cases h; simp_all }
specialize this _ rfl y
simp_all
| .app .., .app .., hxy => by
simp_all; cases hxy.1; simp_all; exact ⟨toNeutral_inj hζ hxy.1, toNormal_inj hζ hxy.2⟩
| .bvar .., .bvar .., _ => by simp_all
end
Floris van Doorn (Oct 30 2025 at 13:44):
It would be nice if there is a tactic that does the have automatically: generalize ζ k to a new variable u and insert casts (or in complicated cases, Eq.rec) to ensure that the rest of the context is still well-typed).
Joachim Breitner (Oct 30 2025 at 13:56):
I was playing around with a variant of generalizing toNeutral_inj like this:
theorem toNeutral_inj {ι : Type u} {κ : Type v} {ζ : κ → Object ι}
{ctx : List (Object ι)} {typ₁ typ₂ : Object ι} (hζ : Injective ζ) : (ht : typ₁ = typ₂) →
{t₁ : Neutralu ζ ctx typ₁} → {t₂ : Neutralu ζ ctx typ₂} → (ht ▸ t₁.toNeutral = t₂.toNeutral) → ht ▸ t₁ = t₂
| ht, .of .., .of .., _ => by specialize hζ ht; cases hζ; dsimp
| ht, .app fn₁ arg₁, .app fn₂ arg₂, heq => by
cases ht
simp_all
cases heq.1
simp_all
have := toNeutral_inj hζ rfl heq.1
have := toNormal_inj hζ heq.2
simp_all
| ht, .bvar .., .bvar .., heq => by
cases ht
simp_all
and the .of case works well, but in this way the pattern match compilation does not recognize that only the diagonal is reachable. (and the termination checker isn't happy)
Aaron Liu (Oct 30 2025 at 15:24):
Floris van Doorn said:
You need that
ζis injective to prove this, right?
No you don't I proved it without assuming anything about ζ
Aaron Liu (Oct 30 2025 at 15:24):
but it's 50 lines I want something shorter
Aaron Liu (Oct 30 2025 at 15:25):
Joachim Breitner said:
And I assume the example is simplified? These functions look like the identity to me (at least on the phone )
No this is directly extracted from my code (hence why I said it was obvious but weirdly annoying)
Aaron Liu (Oct 30 2025 at 15:27):
I can show you my proof if you want
Aaron Liu (Oct 30 2025 at 15:31):
My Proof
Aaron Liu (Oct 30 2025 at 15:49):
In my case having ζ be injective is actually really restrictive, it amounts to saying that you can't have two free variables of the same type, so it really doesn't work require ζ to be injective.
Floris van Doorn (Oct 30 2025 at 16:27):
Nice technique to not require injectivity of zeta. And I didn't know about rw!, that a significant part of the tactic I was requesting.
Here is your proof written in my style:
import Mathlib.Tactic
-- <first code snippet>
attribute [simp] Normalu.toNormal Neutralu.toNeutral
open Function
def Neutral.aux {ι : Type u} {κ : Type v} {ζ : κ → Object ι}
{ctx : List (Object ι)} {typ : Object ι} : Neutral ζ ctx typ → Option κ
| .of k ctx => some k
| _ => none
@[simp] lemma Neutral.aux_of {ι : Type u} {κ : Type v} {ζ : κ → Object ι}
{ctx : List (Object ι)} {k : κ} : Neutral.aux (ζ := ζ) (.of k ctx) = some k := rfl
mutual
theorem toNormal_inj {ι : Type u} {κ : Type v} {ζ : κ → Object ι}
{ctx : List (Object ι)} {typ : Object ι} :
Injective (Normalu.toNormal : Normalu ζ ctx typ → _)
| .ofNeutral .., .ofNeutral .., hxy => by simp_all; exact toNeutral_inj hxy
| .lam .., .lam .., hxy => by simp_all; exact toNormal_inj hxy
theorem toNeutral_inj {ι : Type u} {κ : Type v} {ζ : κ → Object ι}
{ctx : List (Object ι)} {typ : Object ι} :
Injective (Neutralu.toNeutral : Neutralu ζ ctx typ → _)
| .of k .., y, hxy => by
-- The first four lines are basically "`generalize h : ζ k = u` but try harder".
obtain ⟨u, hu⟩ : ∃ u, ζ k = u := ⟨_, rfl⟩
revert y
rw! (castMode := .all) [hu]
intro y hy
cases y
· apply_fun Neutral.aux at hy
simp at hy
rewrite! [← hu] at hy
cases hy
rfl
all_goals { subst hu; simp_all }
| .app .., .app .., hxy => by
simp_all; cases hxy.1; simp_all; exact ⟨toNeutral_inj hxy.1, toNormal_inj hxy.2⟩
| .bvar .., .bvar .., _ => by simp_all
end
(EDIT: simplified)
Aaron Liu (Oct 30 2025 at 17:16):
Currently rw! is in a half-broken state (it works, but can make your goals look ugly (if it does make stuff ugly, dsimp should clear it up)) so I would just use rewrite! for now until I get around to fixing rw!
Last updated: Dec 20 2025 at 21:32 UTC