Zulip Chat Archive
Stream: general
Topic: Multiple overlapping typeclasses
David Loeffler (Oct 01 2024 at 07:34):
How should one deal with lemmas where the "natural" generality involves several typeclass assumptions which contain conflicting data?
For instance, suppose I have a lemma which requires a type X
with a norm, an additive group structure, and a distinguished element "1". The typeclass [NormedAddGroup X]
gives a norm and add-group structure satisfying sensible compatibilities, while [AddGroupWithOne X]
gives an add-group structure and a 1.
However, if you use both at once,
{X : Type*} [NormedAddGroup X] [AddGroupWithOne X]
you don't get the right thing: you get a type with two possibly different add-group structures, one of which one has the norm and the other has the 1.
How do I force the NormedAddGroup structure and the AddGroupWithOne structure to have the same underlying AddGroup structure?
Yaël Dillies (Oct 01 2024 at 07:39):
You make a new NormedAddCommGroupWithOne
typeclass extending NormedAddCommGroup
and AddCommGroupWithOne
Yaël Dillies (Oct 01 2024 at 07:39):
But doesn't NormedRing
fit your purposes?
David Loeffler (Oct 01 2024 at 07:50):
NormedRing
has far more structure; it calls for the existence of multiplication on X
(and for the norm to be submultiplicative), both of which are irrelevant in the case at hand. Probably it's true that there are no interesting examples of NormedAddCommGroupWithOne
s that are not NormedRing
s, but this was only intended as an example of a more general problem.
David Loeffler (Oct 01 2024 at 07:52):
You make a new
NormedAddCommGroupWithOne
typeclass extendingNormedAddCommGroup
andAddCommGroupWithOne
I'm wary of creating a special-purpose typeclass just for one or two lemmas...
Yaël Dillies (Oct 01 2024 at 07:57):
:shrug: That's how typeclasses work. The other option is to have more mixins, eg make NormedAddCommGroup α
take in an [AddCommGroup α]
parameter, rather than implying it
David Loeffler (Oct 01 2024 at 08:03):
So how would this work in practice?
class NormedAddMonoidWithOne (R : Type*)
extends NormedAddGroup R, AddMonoidWithOne R
example (R : Type) [NormedRing R] : NormedAddCommMonoidWithOne R := by infer_instance -- fails
Yaël Dillies (Oct 01 2024 at 08:04):
class NormedAddMonoidWithOne (R : Type*)
extends NormedAddGroup R, AddMonoidWithOne R
instance (R : Type) [NormedRing R] : NormedAddCommMonoidWithOne R where
__ := ‹NormedRing R›
?
David Loeffler (Oct 01 2024 at 08:09):
Are you saying I would need to add an explicit instance of NormedAddMonoidWithOne
for every single pre-existing typeclass that extends both NormedAddGroup
and AddMonoidWithOne
? Then adding the new typeclass doesn't really achieve much. Is there not some way of automating this?
Yaël Dillies (Oct 01 2024 at 08:14):
David Loeffler said:
Are you saying I would need to add an explicit instance of
NormedAddMonoidWithOne
for every single pre-existing typeclass that extends bothNormedAddGroup
andAddMonoidWithOne
?
No, only for the direct common children. Eg you don't need to add NormedField α → NormedAddMonoidWithOne α
because typeclass search already knows NormedField α → NormedRing α
.
Yaël Dillies (Oct 01 2024 at 08:15):
That should be very few typeclasses. In fact, I can't think of another one besides NormedRing
!
David Loeffler (Oct 01 2024 at 08:31):
That should be very few typeclasses. In fact, I can't think of another one besides
NormedRing
!
That's very probably true for this example, but I think the question makes sense more generally.
<rant> To pick up on another point you made (about mixins): I found this one of the most opaque aspects of mathlib when I was first getting used to it: some typeclasses must be used together, while others must not be used together else you get craziness, and there seems to be absolutely no convention as to which; e.g. [NormedSpace ℝ V]
requires a pre-existing NormedAddCommGroup
structure, but you are in for a world of pain if you use it on a type that already has a [Module ℝ V]
structure.</rant>
Kim Morrison (Oct 01 2024 at 10:58):
We all feel your pain, @David Loeffler. This design question is a difficult one, and far from completely settled. The trend has generally been towards converting things to mixins, but obviously it's a lot of work.
Eric Wieser (Oct 01 2024 at 11:14):
I tried to split NormedGroup
from AddCommGroup
long ago, but:
- As Kim says It's a lot of tedious work
- Inevitably proofs break for performance reasons; while unbundling might be faster in principle [citation needed], it won't be faster in the places that optimized the proof for the bundled spelling.
- it makes analysts sad because they now have to write
lean variable [AddCommGroup V] [NormedAddCommGroup V] [Module K V] [NormedSpace K V]
instead of what we have now
lean variable [NormedAddCommGroup V] [NormedSpace K V]
Johan Commelin (Oct 01 2024 at 13:08):
I think we need a good way to handle your "it makes analysts sad". Because only then can we realistically try this out in practice.
variable?
is a good start, but not enough, I think.
Jireh Loreaux (Oct 01 2024 at 14:33):
@Eric Wieser do we have examples where class abbrev
really hurts performance? I ask because, while I thought it might be truly detrimental, it didn't seem to be much/any worse when I tried it in #16975 compared to just adding the relevant instance.
If it didn't significantly impact performance (which would still surprise me), the solution to "it makes analysts sad" would just be to class abbrev
the important structures.
David Loeffler (Oct 01 2024 at 15:58):
Thanks for the comments folks! I am sure that a great deal of thought and energy has gone into trying to find a solution that keeps the pain to a minimum.
I'd be interested in your thoughts on the following particular issue. Suppose you have a series of definitions / theorems all applying in some given setting, described by a list of section-level typeclass variables. (E.g. you have a ring R and an R-module M and a linear map from M to R, ...) Then for one such lemma you want to slightly strengthen the assumptions, maybe "if we are in the above situation but R happens to be a normed ring, then...". If the stronger assumption is implemented as an extension class, rather than a mixin, the only option seems to be to throw away the existing section variables and rebuild the whole edifice of assumptions from scratch.
Is there any hope whatsoever of setting up some syntax to handle this situation? Something like
variable [NormedRing R] instead of [Ring R] in
lemma foo : ...
and having it re-build the existing typeclass assumptions with the more specific typeclass?
Eric Wieser (Oct 01 2024 at 16:16):
I think you can do that with omit [Ring R] in variable [NormedRing R] in
, but you still have to rebuild everything on M
David Loeffler (Oct 01 2024 at 17:31):
Eric Wieser said:
I think you can do that with
omit [Ring R] in variable [NormedRing R] in
, but you still have to rebuild everything onM
I'm not sure that works. If I understand correctly, omit
only reverses the effect of include
, setting a variable back to "include if relevant" rather than "include always". But it doesn't explicitly forbid a typeclass variable from being included, so you can still end up in the dreaded situation of accidentally having two unrelated ring structures on the same type simultaneously.
David Loeffler (Oct 01 2024 at 17:41):
It took me a while to cook up an example, but here's one:
variable (R : Type*) [AddCommGroup R]
/- lemmas lemmas lemmas... -/
omit [AddCommGroup R] in
variable [NormedAddCommGroup R] in
example (a b : R) : ‖a + b‖ ≤ ‖a‖ + ‖b‖ := by
/- tactic state is now:
R : Type u_1
inst✝¹ : AddCommGroup R
inst✝ : NormedAddCommGroup R
a b : R -/
exact norm_add_le a b -- fails, wrong "+"!
Here the call to "norm_add_le" fails because Lean interprets the "+" in the theorem statement as the one from the AddCommGroup instance, while "norm_add_le a b" is a proof of the triangle ineq for the entirely unrelated "+" coming from the NormedAddCommGroup instance.
Eric Wieser (Oct 01 2024 at 17:43):
I don't understand that example, where did AddGroup
come from?
Eric Wieser (Oct 01 2024 at 17:44):
(assuming it's a typo, I think I agree with your diagnosis)
David Loeffler (Oct 01 2024 at 17:59):
Sorry, that was a typo; I realised commutativity was irrelevant to my example but introduced an inconsistency in removing it. Edited.
Johan Commelin (Oct 02 2024 at 04:53):
Hmmm, thanks for that MWE. @Sebastian Ullrich is this intended behaviour or a bug?
Johan Commelin (Oct 02 2024 at 04:53):
Personally, I always thought omit
meant "forbid".
Sebastian Ullrich (Oct 02 2024 at 08:15):
That sounds like a reasonable expectation, feel free to file an issue
David Loeffler (Oct 02 2024 at 09:06):
Sebastian Ullrich said:
That sounds like a reasonable expectation, feel free to file an issue
OK this is now lean#5595.
Damiano Testa (Oct 02 2024 at 09:09):
I like this, since it is going to automatically make include
be "very" scoped, since there is no way to revert an include
to "possibly include"!
David Loeffler (Oct 02 2024 at 09:14):
@Damiano Testa makes the fair point that this change would leave us with no way of precisely reverting the effect of an include
. Some might find that desirable, but others might not. So maybe, rather than changing the behaviour of omit
, we could retain the current behaviour of omit
and have another keyword -- omit!
or forget
perhaps -- for entirely removing a variable from the context.
Riccardo Brasca (Oct 02 2024 at 09:20):
Having both versions seems very reasonable.
Damiano Testa (Oct 02 2024 at 09:31):
How about end h
for moving h
out of the scope? I find that lots of different ways of managing variables cause more problems than they solve.
Damiano Testa (Oct 02 2024 at 09:33):
So you can end a namespace or section, removing all variables in scope, or you can selectively remove a single (or list?) of variables with it as well.
Ruben Van de Velde (Oct 02 2024 at 10:28):
Wasn't omit supposed to be a temporary solution while we moved to sections?
Damiano Testa (Oct 02 2024 at 10:35):
Well, I think that the original suggested change would accelerate the process...
Jireh Loreaux (Oct 02 2024 at 14:21):
I really don't think we should have two versions of omit
. It sounds like a recipe for pain and confusion. If variable
issues are really getting to you, then it probably means you need more section
s.
David Loeffler (Oct 02 2024 at 14:38):
If
variable
issues are really getting to you, then it probably means you need moresection
s.
Using sections does not solve the general problem (although it probably does solve as much of it as the omit!
command would, which is not a perfect solution either). The section-based idiom would be something like
section Ring
variable {R M : Type*} [Ring R] [IsArtinian R] [Module R M] [NoZeroSMulDivisors R M] [etc etc etc...]
[...]
end Ring
section CommRing
variable {R M : Type*} [CommRing R] [IsArtinian R] [Module R M] [NoZeroSMulDivisors R M] [etc etc etc...]
...
and we have to re-declare the entire pile of instance variables at the start of the new section, because it's "not the same R" any more, even though CommRing extends Ring so the new situation is a special case of the old one.
Eric Wieser (Oct 02 2024 at 14:38):
One argument against sections is that it's harder to review diffs, as lemmas which haven't actually changed can appear as deleted and recreated in the diff. If you can leave them in the same place, then this issue goes away
Eric Wieser (Oct 02 2024 at 14:38):
(but this could be solved with something like leaff)
Jireh Loreaux (Oct 02 2024 at 14:43):
Eric Wieser said:
One argument against sections is ...
One argument in favor of sections is that lemmas with similar assumptions get grouped together, which I think is more important (and has more lasting impact), than the diff on GitHub. So, I don't buy your argument.
Jireh Loreaux (Oct 02 2024 at 14:45):
David Loeffler said:
we have to re-declare the entire pile of instance variables at the start of the new section
I agree, this can be annoying. Repeated cases of this may mean we want to refactor Mathlib (e.g., in this case Comm
could become a mixin, although I don't actually think that's a good idea).
David Loeffler (Oct 02 2024 at 14:55):
What I'm hoping for here is precisely some sort of syntax that would allow us to treat an extension class like a mix-in, without actually refactoring it as such – e.g. temporarily "upgrading" a [Ring R] to a [CommRing R] or a [NormedRing R] or whatever, etc, while keeping the rest of the previously-declared instances in force.
Damiano Testa (Oct 02 2024 at 17:26):
I have often wanted a model where the fields of a class are automatically classes themselves and you can lump them together or take them apart at will...
Alex Meiburg (Mar 19 2025 at 15:31):
I could imagine a future where minimal data-carrying classes are the only things that aren't mixins. So e.g. you start with things like a Norm G
, Preorder G
, Topological Space G
, Add G
, Zero G
... then NormedAddGroup G
takes mixin arguments for Add/Neg/Sub/Zero/Norm. Then if you _also_ had an AddCommGroup
or GroupWithOne
in the context, they would share all their data fields so there wouldn't be unification failures.
Of course no one wants to start each file talking about rings with variable [Add G] [Sub G] [Mul G] [Zero G] [One G] [SMul Nat G] [SMul Int G] [Pow G Nat]
, so we could have a few classes that just bundle together commonly coexisting data. Like I could picture a GroupData G extends Mul G, One G, Inv G, Div G, Pow G Nat, Pow G Int
, and then Group
/CommGroup
/Semiring
/NormedGroup
would all take a GroupData
mixin.
But now you would only have to create an appropriate collection of ___Data
classes to cover all your bases, as opposed to one for each combination of properties too, which is definitely a huge shrinkage
Yury G. Kudryashov (Mar 19 2025 at 15:55):
Does it make the exponential blow-up better or worse? If you have a complicated type (e.g., continuous linear maps taking values in the spaces of continuous multilinear maps, add 1 or 2 more layers), then sometimes lean falls to generate an instance (e.g., Norm
), because the expression is too large.
Yury G. Kudryashov (Mar 19 2025 at 15:56):
These types naturally appear when you try to define, e.g., composition of bundled (multi)linear maps add a bundled map.
Alex Meiburg (Mar 19 2025 at 15:59):
I admit I don't know anything about that. It feels like it would be easier to manage the problem here (when you have overlapping typeclasses, the need to essentially create a bespoke new class), easier in the sense of the codebase - but I have no idea what the impact on runtime would look like
Last updated: May 02 2025 at 03:31 UTC