Zulip Chat Archive
Stream: lean4
Topic: regression: typeclass synthesis in structure definitions
Julien Marquet-Wagner (Jun 12 2025 at 12:04):
I have these definitions that work in v4.08.0 but not in v4.19.0 and above.
v4.08.0 accepts the following, but v4.19.0 reports it failed to synthesize the appropriate HMul instances.
structure PointedMagma where
α : Type
one : α
mul : α → α → α
instance : CoeSort PointedMagma Type where
coe := PointedMagma.α
instance (M : PointedMagma) : OfNat M (nat_lit 1) where
ofNat := M.one
instance (M : PointedMagma) : Mul M where
mul := M.mul
/- this is where the problem is -/
structure Group extends PointedMagma where
one_mul : ∀ a : α, 1 * a = a
mul_one : ∀ a : α, a * 1 = a
assoc : ∀ a b c : α, (a * b) * c = a * (b * c)
another example in the same vein
class Hom (α : Type) (β : Type) where
hom : α → β → Type
infixr:10 " ⟶ " => Hom.hom
structure Quiver where
obj : Type
hom : obj → obj → Type
instance : CoeSort Quiver Type where
coe := Quiver.obj
instance (Q : Quiver) : Hom Q Q where
hom := Q.hom
/- same problem here -/
structure CategoryStruct extends Quiver where
id : (a : obj) → a ⟶ a
comp : {a b c : obj} → (a ⟶ b) → (b ⟶ c) → (a ⟶ c)
Julien Marquet-Wagner (Jun 12 2025 at 12:11):
I've tried this on old nightlies:
- works on
nightly-2025-03-22 - breaks on
nightly-2025-03-24
I guess this points to lean4#7302.
Floris van Doorn (Jun 12 2025 at 13:28):
(cc @Kyle Miller )
Kyle Miller (Jun 12 2025 at 16:12):
Thanks for the report @Julien Marquet-Wagner.
The proximal cause for this difference in behavior is that the structure command used to construct a local context of the form
toPointedMagma : PointedMagma
α : Type := toPointedMagma.α
one : toPointedMagma.α := toPointedMagma.one
mul : toPointedMagma.α → toPointedMagma.α → toPointedMagma.α := toPointedMagma.mul
but now the relationship between parent projections and fields has been inverted:
α : Type
one : α
mul : α → α → α
toPointedMagma : PointedMagma := { α := α, one := one, mul := mul }
The motivation for this change is that the first sort of local context was a source of continuing issues, and it was hard to make sure that during elaboration all parents were treated equally.
Kyle Miller (Jun 12 2025 at 16:12):
That your first example ever worked at all is fairly delicate. For example, you should find that the following modification would break it on v4.08.0:
structure Carrier where
α : Type
structure Group extends Carrier, PointedMagma where
one_mul : ∀ a : α, 1 * a = a
mul_one : ∀ a : α, a * 1 = a
assoc : ∀ a b c : α, (a * b) * c = a * (b * c)
So, how did elaboration succeed before?
- The elaborated type of the Mul instance is
(M : PointedMagma) -> Mul M.α - In
one_mul, when you havea : α, by definition this isa : toPointedMagma.α, and so it can pick up on thisMulinstance due to having the right form.
The reason the Carrier parent breaks this scheme is that the new local context is
toCarrier : Carrier
α : Type := toCarrier.α
one : α
mul : α → α → α
and so now we have a : toCarrier.α, which is not of the right form for picking up the Mul instance.
Kyle Miller (Jun 12 2025 at 16:13):
I understand that this is a regression for you, which I am sorry about. There are some features that I added in with lean4#7302 that help with some instances of this, but I can't get them to work here. (See this comment.)
Kyle Miller (Jun 12 2025 at 16:13):
I'd like to get structures-with-carriers to work somehow. One idea that came up before, for another extends problem, is having a way to mark instances so that they get automatically added to the local context (for example, marking the Mul instance so that it gets transformed and added as a local instance).
An idea that just came to mind is adding local unification hints like
unif_hint (α : Type) (p : PointedMagma) where
p =?= { α, one := ???, mul := ??? : PointedMagma }
⊢ α =?= p.α
where each ??? is filled in with the corresponding field in the local context.
Kyle Miller (Jun 12 2025 at 16:13):
The recommended way to work with algebraic structures is as "unbundled" classes, like so.
class PointedMagma (α : Type) where
one : α
mul : α → α → α
instance {α : Type} [PointedMagma α] : OfNat α (nat_lit 1) where
ofNat := PointedMagma.one
instance {α : Type} [PointedMagma α] : Mul α where
mul := PointedMagma.mul
class Group (α : Type) extends PointedMagma α where
one_mul : ∀ a : α, 1 * a = a
mul_one : ∀ a : α, a * 1 = a
assoc : ∀ a b c : α, (a * b) * c = a * (b * c)
Then you can create "bundled" versions, and if you add a CoeSort instance and mark the instance field as being an instance using attribute, then you get a relatively seamless experience between bundled and unbundled points of view.
structure BundledGroup where
α : Type
[inst : Group α]
attribute [instance] BundledGroup.inst
instance : CoeSort BundledGroup Type where
coe := BundledGroup.α
Example: proving a theorem using the Group laws:
example (G : BundledGroup) (x : G) : 1 * x * 1 = x := by
rw [Group.one_mul, Group.mul_one]
Example: creating a bundled group from a Group:
instance : Group Nat where
one := 0
mul := Nat.add
one_mul := Nat.zero_add
mul_one := Nat.add_zero
assoc := Nat.add_assoc
example : BundledGroup := BundledGroup.mk Nat
Kyle Miller (Jun 12 2025 at 16:22):
I've been kicking around the idea of adding a Rocq-like feature where you could mark α as being the carrier using some sort of syntax, and it would automatically construct the CoeSort instance for you. Perhaps this is how one indicates which field should participate in the local unification hint scheme?
There's also been an idea of somehow making it easier to go back and forth between the bundled and unbundled versions, without needing to write any boilerplate. For example, one could imagine something where
structure PointedMagma where
carrier α : Type
one : α
mul : α → α → α
would actually define something like
class PointedMagma (α : Type) where
one : α
mul : α → α → α
structure BundledPointedMagma where
α : Type
[instPointedMagma : PointedMagma α]
attribute [instance] BundledPointedMagma.instPointedMagma
instance : CoeSort BundledPointedMagma Type where
coe := BundledPointedMagma.α
and then an elaborator feature could have (M : PointedMagma) magically be interpreted as (M : BundledPointedMagma). (I tried simulating this with CoeDep, but that doesn't appear to work without explicit ascriptions like (PointedMagma : Type 1).)
Julien Marquet-Wagner (Jun 13 2025 at 13:09):
Thanks for the very detailed answer, @Kyle Miller :)
Last updated: Dec 20 2025 at 21:32 UTC