-
Notifications
You must be signed in to change notification settings - Fork 17.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
proposal: spec: permit non-interface types that support == to satisfy the comparable constraint #52474
Comments
So, to clarify, this would allow the following? func Equal[T comparable](t1, t2 T) bool {
return t1 == t2
}
type Example struct {
Val any
}
func main() {
Equal(Example{""}, Example{""}) // true
Equal(Example{[]int(nil)}, Example{[]int(nil)}) // panic
} However, Without allowing |
Practically, this seems no different to me from just removing the special case that interface types are comparable types that are not in the type set of |
The issue is that an interface can reside at an arbitrary deep level within a struct. (struct of struct of struct...) Hence, it may not be obvious that a comparison may panic. I'm not too convinced and would rather keep the current, stricter, behavior. On the other hand, I see the other issue: it forces the creators of packages to find out in advance whether an interface field should require |
I think ultimately the least confusing thing to allow interface values to match the comparable constraint. That doesn't stop you from allowing values of the comparable interface later. It is admittedly somewhat confusing but this is carpet stuck under furniture that's too heavy to move so it's going to stick up somewhere. |
Unfortunately, I don't think that it would mix well with allowing constraint interfaces (such as |
You can't put interface values in other interfaces so you could still allow values of the comparable interface. |
Edit: (I had misunderstood.your response) The issue lies with asserting an interface value to |
@DeedleFake I agree with what you wrote above. @DeedleFake @zephyrtronium Yes, it is easy to use a struct or array to permit using an arbitrary interface type to satisfy a @jimmyfrasche We could do that. In fact we could do that even after adopting this proposal if we decide to. I'm not personally sure that it's the least confusing approach; see the lengthy discussion in #50646 when we started down that path before changing our minds. I think it's become clear that any approach in this area is confusing. |
I think it is clear that we should either allow |
@Merovius that's not at all clear to me. Doing both seems like a perfectly fine thing to do me—and the best thing to do overall. It's possible I'm missing something, though. |
Well, it also depends on whether there are plans to make all constraint interfaces usable as regular interface types (which was in consideration, I recall). |
@jimmyfrasche If all interface types implement var a any = func() {}
var b comparable = a should be allowed. In that case, I don't see the point of making To me, making i.e. to me, they are both different solutions to the same problem, attacking it from opposite angles. |
You would have to write var a any = func() {}
var b comparable = a.(comparable) replace |
Personally, I think that if we make |
Right, but not all interface types implement any other nonempty interface. But all interface types would implement |
If |
@Merovius How do you think we should address the problem with The |
There is also the option of not using a generic Set but rather an interface-based set (i.e. based on map[interface{}]bool) when one wants to be able to force comparison of potentially non-comparable values. If applicable, then there might not be a need to special-case the generics implementation for composite types that contain interface types. But really it depends on the cases and how widespread there are. Would be great to have real code examples. |
The problem isn't really with "potentially non-comparable values." There are interface types other than I have to duplicate the code, or at least a type-safe wrapper, for every interface type I want to use as a map key. This is exactly the problem that generics should be solving. Out of caution, for situations where comparable types are useful, it was chosen to address code duplication for all comparable types except interface types by introducing a special case for |
Another tact might be to allow the use of (possibly panicking) == with [T any]. Then you would only use the comparable constraint when you want assurances that no incomparables can be passed in. |
The correct approach is redesign the type-system, not patch it, it would make type-system not self-consistent. Comparable should be ordinary interface type, and |
Require to duplicate the type and write custom conversions. That's a hassle, of course. But to me, that's an argument against #51338, not in favor of patching something worse over it. In my opinion, if we think we need #51338, we ought to accept this problem and see how much it comes up - at least at first. There are two other ways (apart from this proposal) how I think we could address it, if need be:
|
We forbade interface-types from implementing |
Just a clarifying question: why it was deemed unacceptable for == to panic? |
@zephyrtronium that's a fair remark. I guess another alternative would be to modify the concept of constraint so that it's not necessarily seen as an interface ( i.e. a constraint applicable to types belonging to a typeset) but rather a constraint applicable to any type of the type system. So that we can constrain interfaces effectively. Typically by using predicates as constraints. ( This would be an extension. We could still have the constraint interfaces so that we can keep But effectively, there would be two kinds of constraints. One would not be "reifable" as a type and stems from the bootstrap of the type system in terms of predicates. (or maybe it could too but with different semantics, I'm not sure) The issue to solve is that so far, we really constrain non-interface types only and we try to constrain interface types indirectly through their typesets. |
@atdiar We have that differentiation today. It's the difference between an interface having only methods and one which also has type elements (or is/embeds |
I think phrases like "comparable, but might panic at runtime" is within currently established jargon and, while a bit less wieldy, not that hard to use. But FWIW arguing about this is off-topic here, I was just trying to point out that I find it confusing to introduce new jargon, if we have not agreed on it. At least what's in the spec leaves a pretty good definition of what's "agreed upon jargon". |
@Merovius There is also the fact that typesets are currently transitive sets and I think it plays well with the concept of interface embedding. IF interfaces are deemed So, of course, we could change |
I must confess that I'm getting a bit lost in the different permutations being discussed in this now-long thread, and so I'm sorry if I'm repeating something that was already said in different words above. My existing intuition is that whenever I use a value of an interface type I'm opting in to various things being checked at runtime rather than compile time, because the purpose of an interface type is to allow dynamically choosing a concrete type dynamically at runtime and so runtime checks are a necessary cost in return for that flexibility. With runtime checks comes the possibility of panics if my code is found incorrect at runtime, which is the risk I take in return for the benefit of choosing types dynamically. With the introduction of type parameters, I extended that intuition to say that if I use a type parameter then I am expecting to fix a specific type at compile time. In return, the compiler should be able to guarantee that my chosen type is suitable and thus ensure that anything type-related which would have been a runtime panic will be a compile-time error instead. My intuitions above make me feel like currently the type parameter handling is overreaching into world of interface types, with this rule aiming to make a certain subset of runtime panics impossible even though I've opted in to that risk by using interface types. It isn't a responsibility of the compiler to prevent me from using interface types in a way which might panic, only to prevent me from using them in a way that would certainly panic. I would therefore be in favor of a design like what is proposed here, where I am allowed to use a value of any interface type (or an otherwise-comparable type containing one) to satisfy the I do see the argument above that it would therefore be no different than If map types didn't already allow the calling program to take this risk when desired then perhaps my existing intuitions here would have developed differently, but with the language as it existed before type parameters the current restriction seems inconsistent and it subverts my ability to opt in to dynamic type checking and the possibility of runtime panics by using interface types. (I am considering the current prose in the spec about what is allowed as a map key to imply that the generic signature of a map type would be |
There are three major definitions of comparable relevant to this thread:
Regardless of definition, there are two uses of comparable:
Note that as interface vaules don't nest T(comparable/1PF) and T(comparable/S) are identical. If we continue to use C(comparable/RPF) and permit T(comparable/RPF) [this is #51338]
If we permit C(comparable/1PF) [this proposal]
If we permit C(comparable/S)
Did I get anything wrong? |
I am exceedingly confused by who is arguing for what behavior here, and must apologize for increasing the amount of confusion in the world by offering an opinion. The Go spec has a definition of comparable, which clearly states that "interface values are comparable". I find it very surprising that the I'm further surprised that the compiler believes I think the simplest available option is to state that all comparable types (under the spec's definition) implement the This means that it is impossible to write a type constraint for values which support |
I think you have it right. The core problem is that the concept of comparability is not clean cut, so with a single comparable type, we cannot cover all cases. The simplest solution would be to add additional predefined interfaces such as hashable, strictlycomparable , etc. Would that work? |
No, I believe everything you say is correct. With the caveat that "could" and "should"/"it would be a good idea" are different.
Lots of things might work, but I don't think this is a good idea at all. We don't really want to expose more pre-declared identifiers unless we absolutely have to. I also don't really think it does solve the problem. The real question is how interface types should behave. If we have an answer to that, we don't need to actually introduce new identifiers for those behaviors, we can just enshrine the behavior we want. |
I opened #52509 to hopefully keep the "comparable and |
Change https://go.dev/cl/401874 mentions this issue: |
I am a little confused as to how the proposal solves the problem stated.
For background, my interest is mainly in making generic versions of (e.g.) As I understand it you can have a
This is despite the fact that comparison of interface values can never panic (as far as I know). I wonder whether the issue (specifically with generic versions of maps, sets etc) is that we're trying to use If we can't make the definition of |
It's not a superset. The types usable as map keys are exactly the same as the types which can be compared using |
Sorry, I put |
@abligh My point was kind of that, if we think the behavior of If we think |
Meta. I think regardless of the outcome of these various proposals what |
Created a sort of counter-proposal #52531 |
Will the following code panic if this proposal is accepted? package main
type s struct {
f any
}
func eq[T comparable](x, y T) bool { return x == y }
func main() {
_ = eq(s{func() {}}, s{func() {}})
} If yes, how does it differ from the following case? package main
func eq[T comparable](x, y T) bool { return x == y }
func main() {
_ = eq(any(func() {}), any(func() {}))
} |
Yes.
That case is not valid, currently or under this proposal. If you mean "it doesn't seem very different, so if we should allow one, why not allow the other?", I agree. |
That's great. So we can't prevent panic anyway. I think this might clarify #52474 (comment) and left for the decision. |
FWIW I think there is an argument to be made that preventing panics is important (not in favor of this proposal, because it doesn't, but e.g. for #51338), even if we didn't do it so far, which goes like this:
This chain would then argue against this proposal as well as #52509, as they don't maintain that meaning. So, if we want to use type element interfaces as variant types at some point, rejecting this proposal and #52509 and accepting #51338 will ultimately lead to the most consistent and simple language. Personally, I don't like that this argument depends on wanting to do something which we haven't yet decided to do. I don't know how likely it is that we will and at what point that would happen. And we might want to solve the "instantiating (I also agree with @jimmyfrasche here, that we should then make sure |
I went back to the discussion in #51338 to evaluate how this proposal compares. As far as I can tell, it does have one advantage — namely, it allows type-assertions to However, that comes with an equivalent disadvantage: a successful type-assertion to |
Thanks for the good discussion. I'm persuaded that this proposal makes the language harder to understand without providing a commensurate improvement. I'm withdrawing it. |
See #52614 for another take on this problem. |
Background
In Go 1.18 we introduced a type constraint
comparable
. The type constraint is satisfied by any type that supports==
and for which==
will not panic. For example, it is satisfied byint
andstring
and by composite types likestruct { f int }
or[10]string
. On the other hand, it is not satisfied by types likeany
orinterface { String() }
orstruct { f any }
or[10]interface{ String() }
.This decision has led to considerable discussion; for example, #50646, #51257, #51338.
When considering whether a type argument satisfies the
comparable
constraint, there are two cases to consider.For an interface type, the rule in the spec is simple: an interface type
T
implements an interfaceI
if "the type set ofT
is a subset of the type set ofI
." By this definition the typeany
does not implementcomparable
: there are many elements in the type set ofany
that are not in the type set ofcomparable
. More generally, no basic interface implementscomparable
. Some general interfaces, such asinterface { ~int }
, implementcomparable
; it is not possible today to use this kind of general interface as an ordinary type, but a type parameter constrained by such a general interface will implementcomparable
.For a non-interface type, the rule is different. A non-interface type
T
implements an interfaceI
if it "is an element of the type set ofI
." For a language-defined type likecomparable
, the language defines that type set. The current definition says thatcomparable
"denotes the set of all non-interface types that are comparable."However, the current implementation is slightly different. In the current implementation, a composite type that includes an element of interface type does not implement
comparable
, although such a composite type is "comparable" according to the definition in the spec. The implementation was written that way based on the belief thatcomparable
should only be implemented by types that will not panic when not used in a comparison. This is a valuable property, but it leads to some complications.For example (this is based on a comment by @Merovius), consider a package "annotated" that implements an annotated value type:
Now consider a package "p" that uses that type, and suppose that package p ensures that it only stores values with comparable types in the
Val
field. It's fine for package p to use the typemap[annotated.Value]bool
. That works because the typeannotated.Value
is comparable according to the language definition. However,annotated.Value
does not implementcomparable
, because the type of the elementVal
is an interface type that does not implementcomparable
. That means that code like this does not work:Since
annotated.Value
does not implementcomparable
, the compiler will reject this code.There is no good workaround for package p in this scenario. There is no way for p to say that it wants a version of the type
annotated.Value
that restrictsVal
to comparable types. It wouldn't be appropriate to change the annotated package, since that package can also be used by other packages that don't want to restrictVal
to comparable types.Proposal
Therefore, we propose that we change the implementation. Arguably the implementation is not quite following the spec here, so there may be no need to change the spec.
The new rule is:
comparable
is all non-interface types that are comparable per the language specAs before:
comparable
if the type set of the interface type is a subset of the type set ofcomparable
comparable
if it is a member of the type set ofcomparable
For example, by this definition,
annotated.Value
implementscomparable
, and the problem outlined above goes away.Note on interface types
This change means that there is little reason to seek the comparable version of a non-interface type
T
, as such types are now always comparable or never comparable. However, people may still want to get the comparable version of an interface type. For example, code might want thefmt.Stringer
type but only permitting comparable types. If we adopt #51338, then people can get this type by writinginterface { comparable; fmt.Stringer }
. However, that is not part of this proposal.Timing
Because of the confusion in this area, and the apparent discrepancy between the spec and the implementation, it may be worth implementing this in the 1.19 release, even though that is soon.
The text was updated successfully, but these errors were encountered: