Zulip Chat Archive
Stream: new members
Topic: Why does synthesizing Membership instance often fail?
aron (May 18 2025 at 21:09):
I have a long piece of code with a lot of different Membership instances between a bunch of different types. I know this isn't recommended, but it fits my domain anyway. But either way I only have one instance between any two types and I always explicitly specify my types so it should never be ambiguous which types I'm referring to in a a ∈ b expression. But even then, I often get errors like
failed to synthesize
Membership A B
even though I do have a Membership instance defined between A and B.
Things work when I specify the instance by name, like instMembershipAB.mem b a but if that works why can Lean often not find the membership instance on its own?
Kevin Buzzard (May 18 2025 at 21:43):
#mwe ?
Kyle Miller (May 18 2025 at 22:29):
It's not just not recommended, but incorrect to define multiple Membership instances.
The type α is an outParam, which means you must ensure that your instances satisfy the constraint that for any given γ there is at most α (in other words, among all the instances, γ determines α). Lean assumes you are satisfying this constraint.
class Membership (α : outParam (Type u)) (γ : Type v) where
/-- The membership relation `a ∈ s : Prop` where `a : α`, `s : γ`. -/
mem : γ → α → Prop
Kevin Buzzard (May 19 2025 at 04:07):
(typo: at most one α)
aron (May 19 2025 at 08:20):
Kevin Buzzard said:
#mwe ?
I wasn't sure how complicated a #mwe I'd need to reproduce this issue but turns out not very complicated at all:
inductive A
inductive B
inductive Container
| a (contents : A)
| b (contents : B)
instance : Membership A Container where
mem container item :=
match container with
| .a contents => item = contents
| .b _contents => False
instance : Membership B Container where
mem container item :=
match container with
| .a _contents => False
| .b contents => item = contents
def testA (container : Container) (a : A) (_h : a ∈ container) := ()
/-- Doesn't work :( -/
def testB (container : Container) (b : B) (_h : b ∈ container) := ()
/-- Referencing instance explicitly does work -/
def testB' (container : Container) (b : B) (_h : instMembershipBContainer.mem container b) := ()
aron (May 19 2025 at 08:24):
Kyle Miller said:
The type
αis an outParam, which means you must ensure that your instances satisfy the constraint that for any givenγthere is at mostα(in other words, among all the instances,γdeterminesα). Lean assumes you are satisfying this constraint.
hm I see what you're saying but this feels like a very limiting restriction. If your type can contain different things it feels only natural that you should be able to define membership for different things?
aron (May 19 2025 at 08:30):
I can understand that if you rely on type inference to tell you what the element type is that that would not always give you the type you want, because well, how could it know which of the different instances you want without telling it?
But if lean does know what the container and element types are because you've explicitly specified the types of the values used in your membership expression, why would that be an issue? surely lean could then unambiguously find the instance in question? and in fact in the example above it does know that it should be looking for an instance of Membership B Container – it doesn't get confused thinking that it should be looking for a Membership A Container. So I don't see why this is not a well supported use case
Edward van de Meent (May 19 2025 at 08:46):
this would all be fixed if the first argument to Membership was a semiOutParam...
Edward van de Meent (May 19 2025 at 08:47):
i.e. the following works:
class Membership' (α : semiOutParam (Type u)) (γ : Type v) where
mem : γ → α → Prop
instance {α : semiOutParam (Type u)} {γ : Type v} [Membership' α γ] : Membership α γ where
mem := Membership'.mem
infix:40 " ∈' " => fun x y => Membership'.mem y x
inductive A
inductive B
inductive Container
| a (contents : A)
| b (contents : B)
instance : Membership' A Container where
mem container item :=
match container with
| .a contents => item = contents
| .b _contents => False
instance : Membership' B Container where
mem container item :=
match container with
| .a _contents => False
| .b contents => item = contents
def testA (container : Container) (a : A) (_h : a ∈' container) := ()
/-- Doesn't work :( -/
def testB (container : Container) (b : B) (_h : b ∈' container) := ()
/-- Referencing instance explicitly does work -/
def testB' (container : Container) (b : B) (_h : instMembership'BContainer.mem container b) := ()
Edward van de Meent (May 19 2025 at 08:48):
(not sure i got the precedence right with the notation, but that's besides the point)
Edward van de Meent (May 19 2025 at 08:51):
also, a quick search suggests that this isn't the first time that this fact has caused (minor) inconvenience
Kyle Miller (May 21 2025 at 19:25):
aron said:
But if lean does know what the container and element types are because you've explicitly specified the types of the values used in your membership expression, why would that be an issue?
Lean takes outParam into account and assumes that you are writing your instances correctly — there's not much more to say. We could work out step-by-step what the consequences of it being an outParam are for why testB fails.
Edward's semiOutParam suggestion might work; it'd need to be thoroughly tested though.
Can you explain why you need more than one Membership instance in your concrete application?
In the linked thread, I'd suggested defining Membership instances for different syntaxes (projections for example). That doesn't work with your inductive Container example, and I think it's cleaner defining explicit Sets and using its membership. That opens up a lot of useful theory too.
import Mathlib.Data.Set.Basic
inductive A
inductive B
inductive Container
| a (contents : A)
| b (contents : B)
def Container.aSet (c : Container) : Set A :=
{ contents | c = .a contents }
def Container.bSet (c : Container) : Set B :=
{ contents | c = .b contents }
def testA (container : Container) (a : A) (_h : a ∈ container.aSet) := ()
def testB (container : Container) (b : B) (_h : b ∈ container.bSet) := ()
Kyle Miller (May 21 2025 at 19:30):
There's also something like this, where you define just one Membership instance:
import Mathlib.Data.Set.Basic
inductive A
inductive B
inductive Container
| a (contents : A)
| b (contents : B)
inductive ContainerObj
| ofA (a : A)
| ofB (b : B)
instance : CoeTail A ContainerObj := ⟨.ofA⟩
instance : CoeTail B ContainerObj := ⟨.ofB⟩
instance : Membership ContainerObj Container where
mem s x :=
match x, s with
| .ofA x, .a contents => x = contents
| .ofB x, .b contents => x = contents
def testA (container : Container) (a : A) (_h : ↑a ∈ container) := ()
def testB (container : Container) (b : B) (_h : ↑b ∈ container) := ()
Unfortunately you need an explicit coercion arrow...
Last updated: Dec 20 2025 at 21:32 UTC