Last active
May 7, 2023 14:43
-
-
Save thejohncrafter/7425065ab9007a7eb668b946270d035e to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/- | |
# Some experiments with unification hints | |
I was playing with category theory in Lean. | |
At some point, I realised I had to use unification hints | |
to solve a problem. I have spent a lot of time figuring this out, | |
and I guess I gathered some valuable experience with this | |
and I'd like to share it with you. | |
Basically, there are two points: | |
* *I found a way to trigger infinite recursion using unification hints* | |
I guess this is some valuable information for the core developers, | |
and that's the main reason why I'm writing this post. | |
* I made some mistakes, and eventually found out why I was mistaken. | |
I guess I'm not the only one who has made these mistakes, there have been | |
other people doing the same things before, and most importantly | |
*there will be other people doing this after me*. | |
Therefore, I think my experience is valuable for the purpose of documentation, | |
and I'm trying to make a discussion that is as clear as possible | |
so that it could fit in Lean 4's documentation, or in some other | |
resources, like tutorials on unification hints. | |
I intended on posting the whole thing here, but unfortunately the message would be way | |
too long for Zulip. Therefore I posted the document on [gist.github.com, here](https://gist.github.com/thejohncrafter/7425065ab9007a7eb668b946270d035e). | |
The file is runnable in Lean 4 (at the time of writing I'm using nightly:2023-04-20). | |
Please feel free to comment, I'd appreciate any feedback! | |
I think comments should preferably be posted here on Zulip for everyone to read, | |
but if it makes sense to you to comment on github then feel free to do so :) | |
Zulip thread: https://leanprover.zulipchat.com/#narrow/stream/270676-lean4/topic/About.20Unification.20Hints | |
-/ | |
universe u v | |
/- | |
## Introduction | |
Let's define some categorical notions. | |
I don't any fully fledged notion of categories, | |
so let's stick with quivers, which already contain all the | |
combinarorics that are relevant to my discussion. | |
-/ | |
-- I introduce this notation because there is a point | |
-- I want to make about it later. | |
class Hom (α : Type u) : Sort (max u v + 1) where | |
hom : α → α → Sort v | |
infixr:10 " ⟶ " => Hom.hom -- \h | |
structure Quiver : Type (max u v + 1) where | |
α : Type u | |
hom : α → α → Sort v | |
instance : CoeSort Quiver (Type u) where | |
coe Q := Q.α | |
instance (Q : Quiver.{u,v}) : Hom.{u,v} Q where | |
hom := Q.hom | |
structure Prefunctor (Q R : Quiver.{u,v}) : Type (max u v) where | |
obj : Q → R | |
map {x y : Q} : (x ⟶ y) → (obj x ⟶ obj y) | |
infixr:26 "⥤" => Prefunctor | |
-- This is the object that is going to cause me trouble. | |
-- Morally, the difficulty is to know the difference | |
-- between `(a, b) ⟶ (a', b')` and `(a ⟶ a') × (b ⟶ b')`. | |
-- This is difficult, because these types are *definitionally equal*! | |
def product_quiver (Q R : Quiver) : Quiver where | |
α := Q × R | |
hom a b := (a.1 ⟶ b.1) × (a.2 ⟶ b.2) | |
infixr:70 "⊗" => product_quiver | |
-- Symmetry and Associativity of the product. | |
-- The difficulty is going to be to guess | |
-- the types of `(symm Q R).map` and `(assoc Q R S).map` | |
def symm (Q R : Quiver) : Q ⊗ R ⥤ R ⊗ Q where | |
obj := fun (a, b) => (b, a) | |
map := fun (f, g) => (g, f) | |
def assoc (Q R S : Quiver) : (Q ⊗ R) ⊗ S ⥤ Q ⊗ (R ⊗ S) where | |
obj := fun ((a, b), c) => (a, (b, c)) | |
map := fun ((f, g), h) => (f, (g, h)) | |
-- These two theorems are easy. In the code I extracted these | |
-- examples from, they are `simp` lemmas. | |
theorem symm_obj {A B : Quiver} (a : A) (b : B) : | |
(symm A B).obj (a, b) = (b, a) := rfl | |
theorem assoc_obj {Q R S : Quiver} (a : Q) (b : R) (c : S) : | |
(assoc Q R S).obj ((a, b), c) = (a, (b, c)) := rfl | |
/- | |
Here's where I get a problem: | |
``` | |
application type mismatch | |
Prod.mk f | |
argument | |
f | |
has type | |
a ⟶ a' : Type ?u.25348 | |
but is expected to have type | |
?m.25356.fst ⟶ ?m.25357.fst : Type ?u.25348 | |
error: type mismatch | |
(g, f) | |
has type | |
(b ⟶ b') × (a ⟶ a') : Type (max ?u.25346 ?u.25348) | |
but is expected to have type | |
Prefunctor.obj (symm A B) ?m.25356 ⟶ Prefunctor.obj (symm A B) ?m.25357 : Type (max ?u.25346 ?u.25348) | |
``` | |
Let's try unification hints to solve this! | |
-/ | |
theorem symm_map {A B : Quiver} | |
{a a' : A} {b b' : B} | |
(f : a ⟶ a') (g : b ⟶ b') : (symm A B).map (f, g) = (g, f) := rfl | |
section | |
/- | |
Here's the solution. This Just Works™ | |
-/ | |
local unif_hint (C D : Quiver) (c : C) (d d' : D) (x : C ⊗ D) where | |
x =?= (c, d) | |
⊢ d' =?= x.2 | |
local unif_hint (C D : Quiver) (c c' : C) (d : D) (x : C ⊗ D) where | |
x =?= (c, d) | |
⊢ c' =?= x.1 | |
theorem symm_map₁ {A B : Quiver} | |
{a a' : A} {b b' : B} | |
(f : a ⟶ a') (g : b ⟶ b') : (symm A B).map (f, g) = (g, f) := rfl | |
end | |
section | |
/- | |
## Infinite Recursion, powered by Unification Hints | |
An earlier version of the UH above was | |
-/ | |
local unif_hint (C D : Quiver) (c : C) (d : D) (x : C ⊗ D) where | |
x =?= (c, d) | |
⊢ d =?= x.2 | |
local unif_hint (C D : Quiver) (c : C) (d : D) (x : C ⊗ D) where | |
x =?= (c, d) | |
⊢ c =?= x.1 | |
/- | |
This still works for `symm`: | |
-/ | |
theorem symm_map₂ {A B : Quiver} | |
{a a' : A} {b b' : B} | |
(f : a ⟶ a') (g : b ⟶ b') : (symm A B).map (f, g) = (g, f) := rfl | |
/- | |
But now, it triggers an unexpected behavior with the similar | |
theorem for `assoc`: | |
``` | |
maximum recursion depth has been reached (use `set_option maxRecDepth <num>` to increase limit) | |
``` | |
Well, that's surprising! | |
-/ | |
theorem assoc_map₁ {Q R S : Quiver} | |
{a a' : Q} {b b' : R} {c c' : S} | |
(f : a ⟶ a') (g : b ⟶ b') (h : c ⟶ c') : | |
(assoc Q R S).map ((f, g), h) = (f, (g, h)) := rfl | |
/- | |
What happened? Let's `set_option trace.Meta.isDefEq true`. | |
``` | |
[Meta.isDefEq] 💥 ?m.30399.fst.fst ⟶ ?m.30400.fst.fst =?= a ⟶ a' | |
[Meta.isDefEq] 💥 ?m.30399.fst.fst ⟶ ?m.30400.fst.fst =?= a ⟶ a' | |
[Meta.isDefEq] 💥 ?m.30399.fst.fst =?= a | |
[Meta.isDefEq] 💥 ?m.30399.fst.1 =?= a | |
[Meta.isDefEq.onFailure] 💥 ?m.30399.fst.1 =?= a | |
[Meta.isDefEq.stuckMVar] found stuck MVar ?m.30399 : ((Q⊗R)⊗S).α | |
[Meta.isDefEq] ✅ ?m.30424 =?= ?m.30399.fst.1 | |
[Meta.isDefEq] ❌ ?m.30425.snd =?= a | |
[Meta.isDefEq] ✅ ?m.30433 =?= ?m.30399.fst.1 | |
[Meta.isDefEq] ❌ ?m.30435.fst =?= a | |
[Meta.isDefEq] ✅ ?m.30444 =?= a | |
[Meta.isDefEq] ❌ ?m.30445.snd =?= ?m.30399.fst.1 | |
[Meta.isDefEq] ✅ ?m.30453 =?= a | |
[Meta.isDefEq] ✅ ?m.30455.fst =?= ?m.30399.fst.1 | |
[Meta.isDefEq] 💥 ?m.30399.fst =?= (a, ?m.30454) | |
[Meta.isDefEq] 💥 ?m.30399.fst =?= (a, ?m.30454) | |
[Meta.isDefEq] ✅ (Q⊗R).α =?= Q.α × R.α | |
[Meta.isDefEq] 💥 ?m.30399.fst.fst =?= a | |
[Meta.isDefEq] 💥 ?m.30399.fst.1 =?= a | |
[Meta.isDefEq.onFailure] 💥 ?m.30399.fst.1 =?= a | |
[Meta.isDefEq.stuckMVar] found stuck MVar ?m.30399 : ((Q⊗R)⊗S).α | |
[Meta.isDefEq] ✅ ?m.30467 =?= ?m.30399.fst.1 | |
[Meta.isDefEq] ❌ ?m.30468.snd =?= a | |
[Meta.isDefEq] ✅ ?m.30476 =?= ?m.30399.fst.1 | |
[Meta.isDefEq] ❌ ?m.30478.fst =?= a | |
[Meta.isDefEq] ✅ ?m.30487 =?= a | |
[Meta.isDefEq] ❌ ?m.30488.snd =?= ?m.30399.fst.1 | |
[Meta.isDefEq] ✅ ?m.30496 =?= a | |
[Meta.isDefEq] ✅ ?m.30498.fst =?= ?m.30399.fst.1 | |
[Meta.isDefEq] 💥 ?m.30399.fst =?= (a, ?m.30497) | |
[Meta.isDefEq] 💥 ?m.30399.fst =?= (a, ?m.30497) | |
[Meta.isDefEq] ✅ (Q⊗R).α =?= Q.α × R.α | |
[Meta.isDefEq] 💥 ?m.30399.fst.fst =?= a | |
[Meta.isDefEq] 💥 ?m.30399.fst.1 =?= a | |
[Meta.isDefEq.onFailure] 💥 ?m.30399.fst.1 =?= a | |
``` | |
Lean repeatedly tries `[Meta.isDefEq.onFailure] 💥 ?m.30399.fst.1 =?= a`! | |
Look at my unification hint: | |
-/ | |
local unif_hint (C D : Quiver) (c : C) (d : D) (x : C ⊗ D) where | |
x =?= (c, d) | |
⊢ c =?= x.1 | |
/- | |
When Lean chooses to use this UH, it has to check `x =?= (c, d)`, | |
which reduces to `x.1 =?= c` and `x.2 =?= d`, which in turns triggers | |
the same UH again... | |
That's why the right UH is | |
-/ | |
local unif_hint (C D : Quiver) (c c' : C) (d : D) (x : C ⊗ D) where | |
x =?= (c, d) | |
⊢ c' =?= x.1 | |
/- | |
which introduces a new metavariable. | |
Basically, this one just says that the elements of `C ⊗ D` are pairs | |
directly formed by a the constructor `Prod.mk`. | |
I'm not sure what the takeaway of this discussion is, but basically | |
> Be careful with your unification hints! | |
I guess the real takeaway is that *if I want to use a unification | |
hint to "guess" the structure of an object, I should introduce | |
new variables for the memebers of this object, rather than using | |
already existing variables, to avoid circular computations.* | |
-/ | |
end | |
section | |
/- | |
Now, if we use the solution I showed at the beginning, we get: | |
-/ | |
local unif_hint (C D : Quiver) (c : C) (d d' : D) (x : C ⊗ D) where | |
x =?= (c, d) | |
⊢ d' =?= x.2 | |
local unif_hint (C D : Quiver) (c c' : C) (d : D) (x : C ⊗ D) where | |
x =?= (c, d) | |
⊢ c' =?= x.1 | |
theorem assoc_map₂ {Q R S : Quiver} | |
{a a' : Q} {b b' : R} {c c' : S} | |
(f : a ⟶ a') (g : b ⟶ b') (h : c ⟶ c') : | |
(assoc Q R S).map ((f, g), h) = (f, (g, h)) := rfl | |
/- | |
This works, yay! | |
-/ | |
end | |
section | |
/- | |
-- | |
## A mistake | |
Let me show another issue I encountered while I was figuring this out. | |
I first thought what I wanted was for Lean to guess that `Quiver.hom` is injective. | |
Well, Lean *already does that*: | |
with (Q R : Quiver) (a b : Q) (c d : R), let's look at the | |
following unification: | |
a ⟶ b =?= c ⟶ d | |
this reduces to | |
Quiver.hom Q a b =?= Quiver.hom R c d | |
then | |
Q.2 a b =?= R.2 c d | |
which gives | |
Q =?= R <- And that's where "injectivity" is checked! | |
a =?= c <- But these two checks are also extremely important | |
b =?= d I encountered a bug which was caused by *me* not | |
getting Lean to forget about these. | |
Let's look at that! | |
(Well, this is not really injectivity, but let's call it that anyway | |
because I guess this makes it easier to understand.) | |
Here was my first shot at the UH: if the arrows are equal, | |
then the categories are equal too. | |
-/ | |
local unif_hint (Q R : Quiver.{u,v}) (c c' : Q) (d d' : R) where | |
Q =?= R | |
⊢ Q.hom c c' =?= R.hom d d' | |
/- | |
But uh-oh! | |
(For some reason I tried to place this UH before | |
the definition of Prefunctor.) | |
``` | |
error: application type mismatch | |
map✝ = map | |
argument has type | |
{x y : Q.α} → (x ⟶ y) → (obj x ⟶ obj y) | |
but function has type | |
({x y : Q.α} → (x ⟶ y) → (obj✝ x ⟶ obj✝ y)) → Prop | |
./././Catlib4/mwe.lean:134:48: error: function expected at | |
m | |
term has type | |
?m.27808 | |
``` | |
-/ | |
structure Prefunctor' (Q R : Quiver.{u,v}) : Type (max u v) where | |
obj : Q → R | |
map {x y : Q} : (x ⟶ y) → (obj x ⟶ obj y) | |
/- | |
What happened? Let's `set_option trace.Meta.isDefEq true` | |
and `set_option trace.Meta.isDefEq.hint true`. | |
[Meta.isDefEq] ✅ {x y : Q.α} → (x ⟶ y) → (obj✝ x ⟶ obj✝ y) =?= {x y : Q.α} → (x ⟶ y) → (obj x ⟶ obj y) | |
[Meta.isDefEq] ✅ Q.α =?= Q.α | |
[Meta.isDefEq] ✅ Q.α =?= Q.α | |
[Meta.isDefEq] ✅ x ⟶ y =?= x ⟶ y | |
[Meta.isDefEq] ✅ obj✝ x ⟶ obj✝ y =?= obj x ⟶ obj y | |
[Meta.isDefEq] ✅ (instHasHomα R).1 (obj✝ x) (obj✝ y) =?= (instHasHomα R).1 (obj x) (obj y) | |
[Meta.isDefEq] ✅ Quiver.hom R (obj✝ x) (obj✝ y) =?= Quiver.hom R (obj x) (obj y) | |
[Meta.isDefEq] ✅ R.2 (obj✝ x) (obj✝ y) =?= R.2 (obj x) (obj y) | |
[Meta.isDefEq] ✅ R.2 =?= R.2 | |
[Meta.isDefEq] ❌ obj✝ x =?= obj x | |
[Meta.isDefEq.onFailure] ✅ R.2 (obj✝ x) (obj✝ y) =?= R.2 (obj x) (obj y) | |
[Meta.isDefEq.hint] R.2 (obj✝ x) (obj✝ y) =?= R.2 (obj x) (obj y) | |
[Meta.isDefEq.hint] ✅ hint hint._@.Catlib4.mwe._hyg.2029 at R.2 (obj✝ x) (obj✝ y) =?= R.2 (obj x) (obj y) | |
[Meta.isDefEq] ✅ Quiver.hom ?m.27662 ?m.27664 ?m.27665 =?= R.2 (obj✝ x) (obj✝ y) | |
[Meta.isDefEq] ✅ Quiver.hom ?m.27663 ?m.27666 ?m.27667 =?= R.2 (obj x) (obj y) | |
[Meta.isDefEq.hint] hint._@.Catlib4.mwe._hyg.2029 succeeded, applying constraints | |
[Meta.isDefEq] ✅ R =?= R | |
------------> I made Lean forget about `❌ obj✝ x =?= obj x`! | |
Here's the first thing: | |
> It is possible to write an ill-formed unification hint | |
> that makes Lean believe the constraints are coherent | |
> while they aren't! | |
Well, here's the zeroth thing: | |
> Unification hints "for injectivity" don't make sense. | |
I wonder how much I can make `isDefEq` lie with wrong unification hints. | |
I tried | |
-/ | |
def add : Nat → Nat → Nat := λ a b => a + b | |
unif_hint (a b c d : Nat) where | |
a =?= c | |
⊢ add a b =?= add c d | |
example : add 0 1 = add 0 2 := rfl | |
/- | |
but fortunately this doesn't work, even though it does trigger my | |
evil unification hint. | |
-/ | |
end | |
section | |
/- | |
I gave another shot at this UH, which made me realize one important | |
behavior of the unification hints | |
-/ | |
local unif_hint (Q R : Quiver.{u,v}) (c c' : Q) (d d' : R) where | |
Q =?= R | |
⊢ c ⟶ c' =?= d ⟶ d' | |
/- | |
This looks fine, as the following code is accepted by Lean. | |
-/ | |
structure Prefunctor'' (Q R : Quiver.{u,v}) : Type (max u v) where | |
obj : Q → R | |
map {x y : Q} : (x ⟶ y) → (obj x ⟶ obj y) | |
/- | |
But this UH *actually doesn't do anything*: | |
`set_option trace.Meta.isDefEq.hint true` reveals that | |
it is never even called. | |
The reason is (seems to be) that the sides of `c ⟶ c' =?= d ⟶ d'` | |
aren't in weak head normal form, which means the UH is never even called. | |
Therefore, here's another takeaway: | |
> Make sure the expressions in the "output" (for lack of a better name) | |
> constraint are in weak head normal form. | |
-/ | |
end | |
section | |
/- | |
## What made me stick with my injectivity mistake | |
Recall the theorem `assoc_map`. | |
I actually got some progress with my "injectivity unification hint": | |
-/ | |
local unif_hint (Q R : Quiver.{u,v}) (c c' : Q) (d d' : R) where | |
Q =?= R | |
⊢ Q.hom c c' =?= R.hom d d' | |
set_option trace.Meta.isDefEq.hint true | |
theorem symm_map₃ {A B : Quiver} | |
{a a' : A} {b b' : B} | |
(f : a ⟶ a') (g : b ⟶ b') : (symm A B).map (f, g) = (g, f) := rfl | |
/- | |
``` | |
error: don't know how to synthesize implicit argument | |
@Prefunctor.map (A⊗B) (B⊗A) (symm A B) (?m.54128 f g) (?m.54129 f g) (f, g) | |
context: | |
A : Quiver | |
B : Quiver | |
a a' : A.α | |
b b' : B.α | |
f : a ⟶ a' | |
g : b ⟶ b' | |
⊢ (A⊗B).α | |
when the resulting type of a declaration is explicitly provided, all holes (e.g., `_`) in the header are resolved before the declaration body is processed | |
``` | |
Well, since the error message has changed, I thought I made some progress! | |
But, as I discussed it earlier, my UH doesn't really make sense, therefore | |
this is a case where I "lied to Lean" with my UH and this made the resulting | |
messages look like I was on the right track. | |
I'm not sure what the takeaway of this last point is. | |
I guess once again here lies something relevant to say about "lying to Lean". | |
Rather, my guess is that with such mistaken unification hints it would be possible | |
to abuse some parts of Lean to produce unexpected behavior, like here where | |
I made argument synthesis fail, or like above where I made the unifier believe | |
two terms were equal when they really weren't. | |
-/ | |
end | |
/- | |
# Final words | |
This is a very long post, thanks for sticking with me until the end! | |
Hopefully I've shared some valuable information with you. | |
I hope that the examples I discuss here can be useful for documentation | |
and for teaching Lean. | |
I think that cateory theory can be a great source of tests for Lean, | |
in that they put the whole system to a test. The difficulty is to make Lean | |
understand statements that are humanly understandable, but that contain | |
subtleties that are actually difficult, even for mathematicians. | |
Let me open this disucssion further. | |
Category theory is actually difficult to understand even -- well, especially -- | |
for humans, because there is a lot of implicit information the expressions | |
that are classically written in a category paper or book. My personal | |
experience with Category Theory For The Working Mathematician was that I | |
struggled a lot to find the type of the many different expressions that | |
lied in diagrams, but I've found that, with exprience, it becomes easier | |
and easier to understand, or guess, what expressions really mean and how | |
they behave. I think formalizing category theory in Lean can serve two purposes: | |
* *Putting Lean to a test.* Here, I discussed some difficulties I encountered, | |
but I've found some other pain points while I was working. I may write some other | |
posts about them. I've also found that writing category theory, or writing | |
code that uses category theory, is a very efficient way of discovering bugs | |
or unexpected behavior in Lean (here, I've told about infinite recursion | |
with unification hints, but I have posted before about some other issues | |
I encountered while I was trying to formalize category theory). | |
* *Heping mathematicians understand category theory.* As I said, I believe | |
it is difficult, especially for beginners, to understand category theory | |
because there is so much implicit information lying in every expression. | |
To me, this is pretty similar to how difficult it is to understand Haskell | |
programs for a beginner. I'd actually like to make a pretty bold statement: | |
*cagegory theory is actually about programming, rather than math*. To me, | |
category theory is a way to formalize arguments about data flowing between | |
objects, even (especially) in the contexts where it is use in mathematics. | |
I believe it would make sense to use Lean to formalize difficult | |
categorical theories, because then it would enable us to generate documentation | |
for expessions, much like a Hoogle for category theory, following the idea | |
that knowing the type of an expression can really help understanding what | |
the expression does, without any need for looking at its details. | |
-/ |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment