Zulip Chat Archive
Stream: new members
Topic: How to reason about the proof for Eq.symm?
Matthias Geier (Oct 09 2025 at 16:49):
I guess this is a valid proof for the symmetry of equality:
example (h : a = b) : b = a :=
h ▸ rfl
I don't understand the operator ▸ yet, so I thought I try something that's easier to understand for me:
example (h : a = b) : b = a :=
match h with
| rfl => rfl
This in turn should be more or less the same as this:
example (h : a = b) : b = a :=
match h with
| Eq.refl a => Eq.refl a
Now comes my question: how can I understand how and why exactly this type-checks?
The input type is a = b, I'm matching with something that expects type a = a and the return type is b = a. Of course, ultimately, all those are the same type, but how does the compiler know?
It's not as easy as all those are equivalent, because if I replace Eq.refl a with Eq.refl b in either position (or both), this ceases to be accepted by Lean.
The reason why I'm asking is that I tried to create my own Eq type (as a learning exercise):
inductive MyEq : α → α → Prop where
| refl (a : α) : MyEq a a
@[match_pattern] def my_rfl {a : α} : Eq a a := Eq.refl a
With this, I can re-write the proof, and this type-checks:
example (h : MyEq a b) : MyEq b a :=
match h with
| MyEq.refl a => MyEq.refl a
However, this doesn't work:
example (h : MyEq a b) : MyEq b a :=
match h with
| my_rfl => my_rfl
I tried it with an without @[match_pattern], which causes two different errors:
Type mismatch
my_rfl
has type
?m.7 = ?m.7
but is expected to have type
MyEq a b
Type mismatch
my_rfl
has type
MyEq a b
but is expected to have type
MyEq b a
Did I make a mistake in my MyEq type?
Or is there something else going on that I'm not aware of?
Kenny Lau (Oct 09 2025 at 16:51):
Matthias Geier said:
how can I understand how and why exactly this type-checks
you can always #print out the proof:
theorem foo {α : Type} {a b : α} (h : a = b) : b = a :=
h ▸ rfl
set_option pp.notation false
set_option pp.explicit true
-- set_option pp.all true
#print foo
Kenny Lau (Oct 09 2025 at 16:52):
Lean is not magic, every notation is required to unfold via 100 layers to an actual Lean expression that the kernel typechecks
Robin Arnez (Oct 09 2025 at 16:52):
However, this doesn't work:
-@[match_pattern] def my_rfl {a : α} : Eq a a := Eq.refl a
+@[match_pattern] def my_rfl {a : α} : MyEq a a := MyEq.refl a
Matthias Geier (Oct 09 2025 at 16:57):
Thanks for the hint with #print, that's very helpful!
Any yes, silly me, I should have used MyEq instead of Eq! with this (and with @[match_pattern]) now it works!
However this still leaves my original question: how does the compiler resolve the types a = b, a = a and b = a?
Kenny Lau (Oct 09 2025 at 16:57):
you're asking a very broad question here, there are many steps and i don't know which step you're referring to
Matthias Geier (Oct 09 2025 at 17:02):
Fair enough. I guess I can narrow it down to 2 questions:
- why does the pattern
Eq.refl aaccept a term of typea = b(even though it should bea = a)? - why is it OK that the
matchstatement returns a term of typea = a, but the return type of the function is supposed to beb = a?
(and I know that those all mean the same thing, but how does the compiler know?)
Kenny Lau (Oct 09 2025 at 17:03):
the concept here is inductive types
Kenny Lau (Oct 09 2025 at 17:03):
https://leanprover.github.io/theorem_proving_in_lean/inductive_types.html
Kenny Lau (Oct 09 2025 at 17:04):
Eq has one constructor, so it comes with the "axiom" that the only way to ever make Eq is by calling the constructor
Kenny Lau (Oct 09 2025 at 17:04):
the compiler or whatever part of lean, it sees that a and b are both free variables
Kenny Lau (Oct 09 2025 at 17:05):
so when you do matching it says ok these are free variables so let's specialise their values to c because of the constructor Eq.refl c
Kenny Lau (Oct 09 2025 at 17:06):
by the philosophy that propositions are types, consider instead the following:
def emphasiseZero (n : Nat) : Nat :=
match n with
| Nat.zero => 0
| Nat.succ m => 37
Kenny Lau (Oct 09 2025 at 17:06):
Nat is similarly an inductive type, with two constructors
Kenny Lau (Oct 09 2025 at 17:07):
so the compiler sees that n is a free variable and it freely assumes that it is formed from either one of the two constructors
Kenny Lau (Oct 09 2025 at 17:07):
for a more concrete answer, have a look at the following outputs:
#check Nat.rec
#check Eq.rec
Kenny Lau (Oct 09 2025 at 17:07):
convince yourself that Nat.rec is the "recursion" that everyone knows
Kenny Lau (Oct 09 2025 at 17:08):
try to connect it to the definition of Nat as an inductive type with two constructors
Kenny Lau (Oct 09 2025 at 17:08):
and then convince yourself that Eq.rec follows the exact same logic
Matthias Geier (Oct 09 2025 at 17:12):
I was hoping I could understand this without having to understand Eq.rec. This is something that's too much for my abilities right now ...
But coming back to your mention of MyEq c: I realized that I was probably re-using the name a for two different things ... but then I saw that this doesn't compile:
example (h : a = b) : b = a :=
match h with
| Eq.refl c => Eq.refl c
It says "Unknown identifier c".
Kenny Lau (Oct 09 2025 at 17:20):
I see, it seems like the first argument is actually anchored in the matching principle
Kenny Lau (Oct 09 2025 at 17:20):
as in, only b is free there, and the matcher substitutes it with the value of the left hand side
Kenny Lau (Oct 09 2025 at 17:21):
example {a b : Nat} (h : 4 + b = a) : a = 4 + b :=
match h with
| Eq.refl _ => _
example {a b : Nat} (h : a = 4 + b) : a = 4 + b :=
match h with
| Eq.refl _ => _
Kenny Lau (Oct 09 2025 at 17:21):
for more context, compare these two examples, and try to put stuff in the underscores, and you'll find out that only (4+b) works for the first example and only a works for the second example, which means that it anchors the LHS and destructs the RHS
Robin Arnez (Oct 09 2025 at 17:23):
If you print out the match, you'll see that it has pulled in b into the match:
def test (h : a = b) : b = a :=
match b, h with
| .(a), Eq.refl a => Eq.refl a
In other words, Eq.refl a type-checks in this place because it has also match b to be a
Robin Arnez (Oct 09 2025 at 17:23):
You can also do this the other way around yourself, although Lean seems to always pull in the second one by default:
def test (h : a = b) : b = a :=
match a, h with
| .(b), Eq.refl b => Eq.refl b
Kenny Lau (Oct 09 2025 at 17:24):
also ignore the "free" stuff i said, that has basically nothing to do with it
Robin Arnez (Oct 09 2025 at 17:25):
You can also introduce a new variable by matching on all variables:
def test (h : a = b) : b = a :=
match a, b, h with
| .(c), c, Eq.refl c => Eq.refl c
Kenny Lau (Oct 09 2025 at 17:28):
what is .()?
Robin Arnez (Oct 09 2025 at 17:30):
That's a so-called inaccessible pattern, which is used when the value within the .() is determined by typing
Matthias Geier (Oct 09 2025 at 17:30):
Wow, thanks! That's a lot to wrap my head around ... I'll play around with this to see if it clarifies the situation for me.
Robin Arnez (Oct 09 2025 at 17:31):
e.g. the .(c) is forced by the typing of Eq.refl c
Aaron Liu (Oct 09 2025 at 17:31):
Kenny Lau said:
I see, it seems like the first argument is actually anchored in the matching principle
The first argument is a parameter and the second argument is an index
Example of a parameter: the n in Fin n
Example of an index: the n in Fin2 n
Last updated: Dec 20 2025 at 21:32 UTC