Zulip Chat Archive
Stream: general
Topic: Instance diamonds depending on `outParam`s
Anne Baanen (Jan 06 2023 at 15:51):
Here's an interesting edge case in the typeclass system I came across in #16919 involving outParam
s. Suppose we have a chain of instances Top α β → Middle α β → Bot α β
, where β
is an outParam
, and both take an instance on β
as a parameter. If the classes on β
arise from diamond inheritance, Lean 3 can't find the desired instance automatically:
set_option old_structure_cmd true
class Root (α : Type) : Type :=
(blah : α)
class Left (α : Type) extends Root α
class Right (α : Type) extends Root α
class Leaf (α : Type) extends Left α, Right α
class Bottom (_ : Type) (β : out_param Type) [Root β]
class Middle (α : Type) (β : out_param Type) [Root β]
class Top (α : Type) (β : out_param Type) [Root β]
instance Top.toMiddle {α β} [Left β] [Top α β] : Middle α β := {}
instance Middle.toBottom {α β} [Right β] [Middle α β] : Bottom α β := {}
set_option trace.class_instances true
set_option trace.type_context.is_def_eq_detail true
instance Top.toBottom {α β} [Leaf β] [Top α β] : Bottom α β :=
by apply_instance -- Fails
The reason for this issue is as follows. After we apply Middle.toBottom
we want to apply Top.toMiddle
, so we need to unify @Middle ?x_3 ?x_4 (@Right.to_Root ?x_4 ?x_5)
arising from Middle.toBottom
with @Middle ?x_7 ?x_8 (@Left.to_Root ?x_8 ?x_9)
arising from Top.toMiddle
. To unify (@Right.to_Root ?x_4 ?x_5) =?= (@Left.to_Root ?x_8 ?x_9)
we need to fill in a value for the metavariables (which will be β
). But the value of this metavariable is only available through unification with Top.toMiddle
, so we are stuck in a dependency cycle.
Anne Baanen (Jan 06 2023 at 15:53):
On the other hand, Lean 4 can successfully find this instance:
import Std.Tactic.Lint
class Root (α : Type) : Type where
(blah : α)
class Left (α : Type) extends Root α
class Right (α : Type) extends Root α
class Leaf (α : Type) extends Left α, Right α
class Bottom (_ : Type) (β : outParam Type) [Root β]
class Middle (α : Type) (β : outParam Type) [Root β]
class Top (α : Type) (β : outParam Type) [Root β]
instance Top.toMiddle [Left β] [Top α β] : Middle α β where
instance Middle.toBottom [Right β] [Middle α β] : Bottom α β where
set_option trace.Meta.synthInstance true
instance Top.toBottom [Leaf β] [Top α β] : Bottom α β :=
inferInstance -- Succeeds
However, the dangerous instance linter is not satisfied:
/- The `dangerousInstance` linter reports:
SOME INSTANCES ARE DANGEROUS
During type-class search, they produce subgoals like `Group ?M`.
Try marking the dangerous arguments as implicit instead. -/
#check @Top.toMiddle /- generates subgoals with metavariables: argument 3 inst✝¹ : Left ?β -/
#check @Middle.toBottom /- generates subgoals with metavariables: argument 3 inst✝¹ : Right ?β -/
Anne Baanen (Jan 06 2023 at 15:59):
This seems to be caused by Lean 4 going ahead and solving the Leaf ?β
instance with backtracking. For example, if I try to confuse matters by adding in more Leaf
instances, we see the backtracking:
instance Top.toBottom [correctInst : Leaf β] [badInst1 : Leaf Nat] [badInst2 : Right Int] [Top α β] : Bottom α β :=
inferInstance -- succeeds
/--
[] new goal Bottom α _tc.0 ▶
[] ✅ apply @Middle.toBottom to Bottom α ?m.332 ▼
[tryResolve] ✅ Bottom α ?m.332 ≟ Bottom α ?m.332 ▼
[synthInstance] 💥 Right ?m.332 ▶
[synthInstance] 💥 Right ?m.332 ▼
[] new goal Right ?m.332 ▼
[instances] #[@Leaf.toRight, badInst2]
[] 💥 apply badInst2 to Right ?m.332 ▼
[tryResolve] 💥 Right ?m.332 ≟ Right Int
[] new goal Right _tc.0 ▼
[instances] #[@Leaf.toRight, badInst2]
[] ✅ apply badInst2 to Right Int ▶
[resume] propagating Right Int to subgoal Right Int of Bottom α Int ▶
[] ❌ apply @Top.toMiddle to Middle α Int ▶
[] ✅ apply @Leaf.toRight to Right ?m.356 ▼
[tryResolve] ✅ Right ?m.356 ≟ Right ?m.356
[] new goal Leaf _tc.0 ▶
[] ✅ apply badInst1 to Leaf Nat ▼
[tryResolve] ✅ Leaf Nat ≟ Leaf Nat
[resume] propagating Leaf Nat to subgoal Leaf Nat of Right Nat ▶
[resume] propagating Right Nat to subgoal Right Nat of Bottom α Nat ▶
[] ✅ apply @Top.toMiddle to Middle α Nat ▶
[] ✅ apply @Leaf.toLeft to Left Nat
[resume] propagating Left Nat to subgoal Left Nat of Middle α Nat ▶
[] ❌ apply inst✝ to Top α Nat ▶
[] ✅ apply correctInst to Leaf β ▶
[resume] propagating Leaf β to subgoal Leaf β of Right β ▶
[resume] propagating Right β to subgoal Right β of Bottom α β ▶
[] ✅ apply @Top.toMiddle to Middle α β ▶
[] ✅ apply @Leaf.toLeft to Left β
[resume] propagating Left β to subgoal Left β of Middle α β ▶
[] ✅ apply inst✝ to Top α β ▶
[resume] propagating Top α β to subgoal Top α β of Middle α β ▶
[resume] propagating Middle α β to subgoal Middle α β of Bottom α β ▶
[] result Middle.toBottom
-/
Anne Baanen (Jan 06 2023 at 16:02):
Finally, if we follow the dangerousInstance
linter and turn the instance parameters into implicits, the example breaks in Lean 4 for the same reason as in Lean 3:
import Std.Tactic.Lint
class Root (α : Type) : Type where
(blah : α)
class Left (α : Type) extends Root α
class Right (α : Type) extends Root α
class Leaf (α : Type) extends Left α, Right α
class Bottom (_ : Type) (β : outParam Type) [Root β]
class Middle (α : Type) (β : outParam Type) [Root β]
class Top (α : Type) (β : outParam Type) [Root β]
instance Top.toMiddle {_ : Left β} [Top α β] : Middle α β where
instance Middle.toBottom {_ : Right β} [Middle α β] : Bottom α β where
set_option trace.Meta.synthInstance true
instance Top.toBottom [Leaf β] [Top α β] : Bottom α β :=
inferInstance -- Failed
/-
[Meta.synthInstance] ❌ Bottom α β ▼
[] new goal Bottom α _tc.0 ▶
[] ✅ apply @Middle.toBottom to Bottom α ?m.322 ▶
[] ❌ apply @Top.toMiddle to Middle α ?m.322 ▼
[tryResolve] ❌ Middle α ?m.322 ≟ Middle ?m.340 ?m.339 ▼
[synthInstance] 💥 Right ?m.339 ▼
[] new goal Right ?m.339 ▶
[] ✅ apply @Leaf.toRight to Right ?m.339 ▶
[] 💥 apply inst✝¹ to Leaf ?m.339 ▼
[tryResolve] 💥 Leaf ?m.339 ≟ Leaf β
[synthInstance] 💥 Left ?m.339 ▶
[synthInstance] 💥 Right ?m.339 ▶
[synthInstance] 💥 Left ?m.339 ▶
-/
Anne Baanen (Jan 06 2023 at 16:07):
I'm not entirely sure what the conclusion is supposed to be. We can see it is possible to make this construction work, and it certainly does arise naturally in mathlib (since I came across it in #16919), so I'd love for this to just work unconditionally. But the elaboration order to make this work without backtracking is highly nontrivial.
Anne Baanen (Jan 06 2023 at 16:15):
Perhaps the conclusion is that we shouldn't have instances that take instance parameters depending on outParam
s only, and instead only do constructions like this:
class Root (α : Type) : Type where
(blah : α)
class Left (α : Type) extends Root α
class Right (α : Type) extends Root α
class Leaf (α : Type) extends Left α, Right α
class ParamProvider (α : Type) (β : outParam Type)
class Bottom (α : Type) (β : Type) [ParamProvider α β] [Root β]
class Middle (α : Type) (β : Type) [ParamProvider α β] [Root β]
class Top (α : Type) (β : Type) [ParamProvider α β] [Root β]
instance Top.toMiddle [ParamProvider α β] [Left β] [Top α β] : Middle α β where
instance Middle.toBottom [ParamProvider α β] [Right β] [Middle α β] : Bottom α β where
instance Top.toBottom [ParamProvider α β] [Leaf β] [Top α β] : Bottom α β :=
inferInstance
That is, unbundled inheritance from FunLike
and SetLike
.
Last updated: Dec 20 2023 at 11:08 UTC