Zulip Chat Archive

Stream: lean4

Topic: Equality between indexed types


Scott Godwin (May 08 2023 at 06:37):

Is there a general pattern for handling equality of two terms of an indexed type where the indexes are not definitionally equal but are propositionally equal? The equality can't be stated since they aren't definitionally equal, and trying to do rewrites in the definition itself introduces a lot of Eq.mp/Eq.mpr which makes reasoning about them extremely hard in my experience. I can try to come up with an example, but I've run into this a lot and just wanted to see if there's an agreed upon approach.

I know in Agda, for example, you can state rewrite rules for propositional equality and then you can state the equality since they will unify.

Shreyas Srinivas (May 08 2023 at 07:04):

Are you looking for coercions by any chance? There is the Coe typeclass if that is the case.

Johan Commelin (May 08 2023 at 07:06):

There is also HEq, which is a heterogeneous equality.

Scott Godwin (May 08 2023 at 07:24):

Here is a highly contrived example that illustrates what I mean:

inductive Foo : Nat  Type where
  | foo : (n : Nat)  Foo n

def bar {n : Nat} : Foo n  Foo n  Type := sorry

def baz {m n : Nat} (f₁ : Foo (m + n)) (f₂ : Foo (n + m)) : bar f₁ f₂ := sorry

In the definition of baz itself, I can't call bar f₁ f₂ because their indexes are not definitionally equal, but they are propositionally equal.

Shreyas Srinivas said:

Are you looking for coercions by any chance? There is the Coe typeclass if that is the case.

I haven't actually used Coe before, but it doesn't seem like it's appropriate in this situation?

Johan Commelin said:

There is also HEq, which is a heterogeneous equality.

Sometimes you can do that, but like the example above, I don't see how you'd use it.

Henrik Böving (May 08 2023 at 07:26):

I don't know myself how to make proper use of HEq but you can go like so if you want:

inductive Foo : Nat  Type where
  | foo : (n : Nat)  Foo n

def bar {n : Nat} (lhs : Foo n) (rhs : Foo m) (h : n = m) : Prop := lhs = h  rhs

def baz {m n : Nat} (f₁ : Foo (m + n)) (f₂ : Foo (n + m)) : bar f₁ f₂ (Nat.add_comm _ _) := sorry

or alternatively:

inductive Foo : Nat  Type where
  | foo : (n : Nat)  Foo n

def bar {n : Nat} (lhs : Foo n) (rhs : Foo n) : Prop := lhs = rhs

def baz {m n : Nat} (f₁ : Foo (m + n)) (f₂ : Foo (n + m)) : bar f₁ ((Nat.add_comm _ _)  f₂)  := sorry

Shreyas Srinivas (May 08 2023 at 07:33):

Coercions will work well for this example. You need to define coercions for Foo (n+m) and Foo (m+n). Alternatively in this particular case Nat.add_comm _ _ \t term also helps

Shreyas Srinivas (May 08 2023 at 07:34):

(Replace \t with the Unicode variant)

Kevin Buzzard (May 08 2023 at 07:40):

Rather than using the coercion framework people usually get around this by writing a bespoke wrapper for Eq.rec and training the simplifier on it. See for example docs#category_theory.eq_to_hom in the lean 3 category theory library; it produces a morphism A -> B from a proof of A=B.

This issue is a standard thorn in the side of dependent type theory

Shreyas Srinivas (May 08 2023 at 09:13):

You might also like to see how Fin n works.

Shreyas Srinivas (May 08 2023 at 09:26):

docs4#Fin

Scott Godwin (May 08 2023 at 09:30):

Thanks for the responses. I'll admit that when casting starts getting involved, I start getting a bit lost. It obviously makes sense to do it, but then I run into situations where I need to give a term of an indexed type SomeType (... a ... ) but I instead I have something like SomeType (... (Eq.mp _ a) ...) and I'm just not sure how to proceed. For example, this is the actual error message I'm stuck on (trying to prove strong normalization of the STLC as an exercise):

type mismatch
  h
has type
  HereditarilyNormalizing
    (Expr.application
      (application fs
        (Eq.mpr
          (_ :
            (Γ  arrow (Ty.arrow f.τ τ') (List.map (fun f => f.τ) fs)) =
              (Γ  arrow τ' (List.map (fun f => f.τ) fs ++ [f.τ])))
          (Eq.mpr
            (_ :
              (Γ  arrow τ' (List.map (fun f => f.τ) fs ++ [f.τ])) =
                (Γ  List.foldr (fun τ' τ => Ty.arrow τ' τ) (Ty.arrow f.τ τ') (List.map (fun f => f.τ) fs)))
            (Eq.mp
              (_ :
                (Γ  arrow τ' (List.map (fun f => f.τ) (fs ++ [f]))) =
                  (Γ 
                    List.foldr (fun τ' τ => Ty.arrow τ' τ) (List.foldr (fun τ' τ => Ty.arrow τ' τ) τ' [f.τ])
                      (List.map (fun f => f.τ) fs)))
              (Expr.var
                (Eq.mpr
                  (_ :
                    (Γ  arrow τ' (List.map (fun f => f.τ) (fs ++ [f]))) =
                      (Γ  List.foldr (fun τ' τ => Ty.arrow τ' τ) τ' (List.map (fun f => f.τ) fs ++ [τ])))
                  (Eq.mpr
                    (_ :
                      (Γ  List.foldr (fun τ' τ => Ty.arrow τ' τ) τ' (List.map (fun f => f.τ) fs ++ [τ])) =
                        (Γ  List.foldr (fun τ' τ => Ty.arrow τ' τ) (Ty.arrow τ τ') (List.map (fun f => f.τ) fs)))
                    a)))))))
      e') : Type
but is expected to have type
  HereditarilyNormalizing (Expr.application (application fs (Expr.var a)) e') : Type

I'm sure it could be cleaned up significantly, but was just trying to get it to typecheck first. I don't really expect anyone to figure it out from that and I don't really know of a good way to create a MWE, just noting that my types become polluted with all these Eq.mp/Eq.mprs after doing these kinds of casts.

Kyle Miller (May 08 2023 at 09:38):

In addition to docs4#CategoryTheory.eqToHom, two other mathlib case studies in having functions to manipulate indices into the right form are docs4#SimpleGraph.Walk.copy and docs4#Quiver.Hom.cast

Kyle Miller (May 08 2023 at 09:40):

I didn't actually test this, but a way this design pattern could apply in this case is like so:

inductive Foo : Nat  Type where
  | foo : (n : Nat)  Foo n

def bar {n : Nat} : Foo n  Foo n  Type := sorry

def Foo.cast (f : Foo n) (h : n = m) : Foo m := h  f

def baz {m n : Nat} (f₁ : Foo (m + n)) (f₂ : Foo (n + m)) : bar f₁ (f₂.cast (add_comm _ _)) := sorry

Kyle Miller (May 08 2023 at 09:41):

Unrestricted Eq.mpr/Eq.rec/etc. tend to be annoying to work with; having a wrapper helps constrain exactly what is being rewritten.

Kyle Miller (May 08 2023 at 09:42):

If you look in the SimpleGraph.Walk module, you can take a look at how helper lemmas for the wrapper are proved. You usually write lemmas where the index equalities are between variables (rather than arbitrary expressions), then start the proof with subst_vars. Many times then the proof of the helper lemmas is rfl after this.

Eric Wieser (May 08 2023 at 09:49):

docs#tensor_power.cast is another example. Note this is an example of it often being nicer not to just use the triangle.

Eric Wieser (May 08 2023 at 09:50):

(docs#fin.cast being the other main example of that pattern)

Scott Godwin (May 08 2023 at 09:56):

Thanks, I'll try defining cast and the corresponding lemmas on the related indexed types and see where that gets me.

Scott Godwin (May 08 2023 at 11:59):

Spent some time on this, defining all the cast lemmas etc and was able to get the proof to go through (or at least the part I was having trouble with, still have a couple more to go)! Thanks again for the help everyone!


Last updated: Dec 20 2023 at 11:08 UTC