Zulip Chat Archive
Stream: new members
Topic: Proofs and casts
Nelson Elhage (Aug 05 2025 at 23:18):
Hi folks,
I'm learning Lean, and I've been playing with the Vect type from FPiL chapter 7. I'm struggling to figure out how to prove things about expressions involving casts. Here's a minimal concrete example:
inductive Vect (α : Type u) : Nat → Type u where
| nil : Vect α 0
| cons : α → Vect α n → Vect α (n + 1)
namespace Vect
def head : (v : Vect α (Nat.succ n)) → α
| cons a _ => a
-- If we define `append` this way, we don't need any casts
def append (lhs : Vect α m) (rhs : Vect α n) : Vect α (n + m) :=
match lhs with
| nil => rhs
| cons x xs => cons x (append xs rhs)
-- And we can easily prove a simple theorem about the interaction of
-- `head` and `append`
theorem head_append {n m : Nat} (v : Vect α (m + 1)) (rhs : Vect α n) :
head v = head (append v rhs) := by
cases v
simp [head, append]
-- We could instead define `append` this way, which works, but now
-- requires a cast
def append' (lhs : Vect α m) (rhs : Vect α n) : Vect α (m + n) :=
match lhs with
| nil => Nat.zero_add .. ▸ rhs
| cons x xs => Nat.add_comm .. ▸ cons x (append xs rhs)
-- But now I get stuck with the proof
theorem head_append' {n m : Nat} (v : Vect α (m + 1)) (rhs : Vect α n) :
head v = head (Nat.add_comm .. ▸ append' v rhs) := by
cases v
simp [head, append]
-- At this point, we have a goal of the form:
-- ⊢ a¹ =
-- match ⋯ ▸ (cons a¹ a).append' rhs with
-- | cons a a_1 => a
--
-- We're "stuck" on the cast, and I haven't figured out how to make
-- progress.
From reading more I get the impression the usual answer is "don't do that" i.e. don't use the Vect type as defined, and instead work with something like a (l : List) paired with a proof h : len l = n. But for the sake of learning and understanding, I'm curious how to push this approach through to completion.
Eric Wieser (Aug 05 2025 at 23:24):
This works:
@[simp]
theorem cast_cast {m n p} (v : Vect α m) (h₁ : m = n) (h₂ : n = p) : h₂ ▸ h₁ ▸ v = (h₁.trans h₂) ▸ v := by
cases h₁; cases h₂; rfl
theorem head_append' {n m : Nat} (v : Vect α (m + 1)) (rhs : Vect α n) :
head v = head (Nat.add_comm .. ▸ append' v rhs) := by
cases v
simp [head, append']
Eric Wieser (Aug 05 2025 at 23:26):
Or more generally
-- I have no idea how to name this
@[simp]
theorem triangle_triangle {m n p} {F : α → Type _} (v : F m) (h₁ : m = n) (h₂ : n = p) : h₂ ▸ h₁ ▸ v = h₁.trans h₂ ▸ v := by
cases h₁; cases h₂; rfl
Eric Wieser (Aug 05 2025 at 23:26):
@Kyle Miller, do you think this lemma would be a bad idea in core?
Aaron Liu (Aug 05 2025 at 23:35):
This is docs#cast_cast combined with docs#eqRec_eq_cast (or docs#Eq.rec_eq_cast)
Kyle Miller (Aug 05 2025 at 23:39):
Nelson Elhage said:
"don't do that"
Yes, don't do that, but not for the reason you're thinking. What you want is to define a Vect.cast function that's specifically for changing the length. You then use it whenever you need to deal with a definitional equality problem. You don't use the triangle operator.
There are a handful of times I've mentioned this on Zulip, but here's the last one I found via search:
Kyle Miller (Aug 05 2025 at 23:43):
Here are some exercises about Vec that I gave my students in a graduate course:
Vec exercises
Kyle Miller (Aug 05 2025 at 23:43):
It pushes the Vec.cast approach. Many times you can write v.cast (by omega) or something similar to have Lean figure out how to line things up when there's a mismatch.
Nelson Elhage (Aug 05 2025 at 23:44):
Hm, I tried using a Vect.cast approach and ran into similar challenges, but I think they're related to my next question: which is that I'm trying to understand why that cast_cast lemma helps. It doesn't let us directly get rid of the cast, but I guess simp is able to use it to add a new cast back to the original type, and then remove them, or something?
Perhaps said another way, I don't feel like I fully understand why we can't reduce the match with a cast
Kyle Miller (Aug 05 2025 at 23:47):
Vect.cast is superior to using the triangle because it constrains the equality to the natural number. Using triangle creates equalities between types, and these lead to many complications. Furthermore, you can write simp lemmas that state equations about Vect.cast in relation to other Vect operations.
Generally, you want to write simp lemmas that push casts 'outward' (like f v.cast = (f v).cast). Eventually they all collect at the root of the expression and cancel out, ideally.
Kyle Miller (Aug 05 2025 at 23:48):
Nelson Elhage said:
I don't feel like I fully understand why we can't reduce the match with a cast
You can reduce a match of a constructor application. There's a cast, so it's not a constructor application.
Casts can only reduce once the equality becomes a = a, both sides the same.
Kyle Miller (Aug 05 2025 at 23:50):
If you have some lemmas about cast, cons, and append, then you'd likely be able to push the cast into cons/append and reduce the match.
This is much harder to express using the triangle operator, given how generic it is.
Eric Wieser (Aug 05 2025 at 23:50):
Should _root_.cast also be preferred to the triangle? It seems to have more lemmas.
Aaron Liu (Aug 05 2025 at 23:51):
I don't like it
Aaron Liu (Aug 05 2025 at 23:51):
the triangle gives you access to a stronger proof
Kyle Miller (Aug 05 2025 at 23:52):
We don't want to be working with raw Eq.rec in terms, so can I say "neither" @Eric Wieser?
Aaron Liu (Aug 05 2025 at 23:52):
for example if you see cast ⋯ a in your goal you only know that Vector Nat n = Vector Nat m but if you see ⋯ ▸ a then you know that n = m
Aaron Liu (Aug 05 2025 at 23:53):
Kyle Miller said:
We don't want to be working with raw
Eq.recin terms, so can I say "neither" Eric Wieser?
why not?
Kyle Miller (Aug 05 2025 at 23:53):
Why do you know that n = m @Aaron Liu? It's just as unconstrained.
Aaron Liu (Aug 05 2025 at 23:54):
no since the term is (⋯ : n = m) ▸ a the proof is exposed compared to cast (⋯ : Vector Nat n = Vector Nat m) a it's buried inside a congrArg
Aaron Liu (Aug 05 2025 at 23:54):
of course if you have (⋯ : Vector Nat n = Vector Nat m) ▸ a then you're screwed
Aaron Liu (Aug 05 2025 at 23:55):
but most tactics don't look inside the proofs so having the equality be on the outside is a benefit
Kyle Miller (Aug 05 2025 at 23:56):
Ok, you're saying it's possible for it to be exposed. Sure.
The point still stands, Vector.cast is the way to go. That way you know for sure it will always be a Nat equality, and you stand a chance at writing lemmas about it that preserve the structure.
Kyle Miller (Aug 05 2025 at 23:57):
(Oh, hi @Nelson Elhage, just noticed who you were :wave: I remember you teaching me Scheme back in Jan 2009.)
Aaron Liu (Aug 05 2025 at 23:58):
You can write lemmas about the triangle too
Aaron Liu (Aug 05 2025 at 23:58):
And these lemmas apply universally whereas for custom cast functions may be nicer but you have to write the lemmas over and over again
Aaron Liu (Aug 05 2025 at 23:59):
Not saying it's bad to have custom cast functions just that we should also have lemmas about the triangle
Aaron Liu (Aug 05 2025 at 23:59):
Or docs#cast
Nelson Elhage (Aug 06 2025 at 00:44):
@Kyle Miller Ha, small world! The tables have turned, it seems :)
Nelson Elhage (Aug 06 2025 at 00:49):
You can reduce a
matchof a constructor application. There's a cast, so it's not a constructor application.Casts can only reduce once the equality becomes
a = a, both sides the same.
Okay, I think that was my first guess. So to the question of how/why cast_cast works, I'm guessing simp is able to use it to synthesize a cast back to the original type, and then get rid of the cast entirely via cast_eq (or the equivalent for the triangle)? Is that an accurate picture of what's going on there?
Nelson Elhage (Aug 06 2025 at 00:51):
(and in particular cast_cast lets you synthesize a cast and then merge the casts, at which point you have a no-op cast, which can be deleted via cast_eq)
Kyle Miller (Aug 06 2025 at 00:55):
Yeah, that's what it looks like it's doing. One cast uses n + (m + 1) = m + 1 + n, the next uses m + 1 + n = n + (m + 1), and the casts compose to be a single cast n + (m + 1) = n + (m + 1), and that's a no-op.
I checked simp? with Eric's proof, and that gives simp only [head, append', Nat.add_eq, cast_cast], so I guess simp doesn't end up eliminating the cast. What might be happening is that the no-op cast is definitionally equal to the identity function, so the match is able to reduce.
Kyle Miller (Aug 06 2025 at 01:00):
Here's an approach to it using split and some properties of heterogeneous equality:
theorem head_append''' {n m : Nat} (v : Vect α (m + 1)) (rhs : Vect α n) :
head v = head (Nat.add_comm .. ▸ append' v rhs) := by
cases v
simp only [head, append']
split
rename_i h
have := heq_of_eq h
simp only [eqRec_heq_iff_heq] at this
simp at this
exact this.1
Sometimes it's easier to work with docs#HEq to start eliminating casts. Heterogeneous equality allows the types of the LHS and RHS to not be the same, and so it's able to "consume" the casts.
Nelson Elhage (Aug 07 2025 at 17:42):
One more question as I return to playing around and trying to build my understanding: Is there a way to make Lean expand the ⋯ in the output so I can see the full details of what's going on?
Aaron Liu (Aug 07 2025 at 17:42):
set_option pp.proofs true
Nelson Elhage (Aug 07 2025 at 17:44):
And is there a manual or reference I could have found that in? I looked in the reference manual but couldn't even find a mention of set_option or any list of common options.
Aaron Liu (Aug 07 2025 at 17:44):
no idea
Kyle Miller (Aug 07 2025 at 18:38):
If you hover over it in the Infoview it should tell you about the option
Nelson Elhage (Aug 07 2025 at 18:39):
I've been using the emacs mode instead of VS Code, which may be hamstringing me.
Kyle Miller (Aug 07 2025 at 18:39):
I'm an emacs user for everything but Lean. I find the UI to be too useful to give up, even though I still don't find editing in VS Code to completely comfortable.
𝚠𝚘𝚓𝚌𝚒𝚎𝚌𝚑 𝚗𝚊𝚠𝚛𝚘𝚌𝚔𝚒 (Aug 07 2025 at 18:44):
Aaron Liu said:
And these lemmas apply universally whereas for custom cast functions may be nicer but you have to write the lemmas over and over again
I agree with Aaron here, but I think Eq.rec is not the right operator because, at least using the default triangle elaborator, you'll often end up with the relevant proof buried inside congrArgs. I made an elementary investigation into better operators that enforce that the proof you need is stored in the syntax as one of the arguments in this repo.
Kyle Miller (Aug 07 2025 at 18:44):
I haven't tried it, but there seems to be a fork of emacs-mode that people like:
I'm not sure what the current state of emacs-mode development is. It would be nice to know if someone was actively maintaining it to have feature parity with the VS Code extension.
Last updated: Dec 20 2025 at 21:32 UTC