-
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 values to have type "comparable" #51338
Comments
To be clear, this proposal is for Go 1.19 or later. CC @griesemer |
Amusingly, I wrote my first real generic code today and then immediately ran into this situation, in almost exactly the way described in the writeup. My generic set type: type Set[T comparable] map[T]T I initially tested it with some simple cases involving var _ Set[exampleInterface]
var _ Set[exampleComparableStruct]
var _ Set[*exampleUncomparableStruct] // pointers are comparable regardless of target type
type exampleInterface interface {
comparable
Boop()
}
type exampleComparableStruct struct {
v [2]int
}
type exampleUncomparableStruct struct {
f func(int)
} This fails at compile time with the following error:
I understand that this is one of the cases you already listed as what this proposal would allow:
However, I ended up here mostly by luck, because this error message happened to exactly match the text you used to describe the situation that would become valid. This error message was confusing to me because it just states something I know to be true: I intentionally embedded I totally understand that making this work is not on the table until Go 1.19 at the earliest, but I wonder if it would be possible in the meantime to improve this error message to at least make a more direct statement about what I did wrong here:
Perhaps I'm misunderstanding, but I believe from this writeup (and from my own further experimentation) that it's the declaration of the variable that is invalid, not the interface declaration itself. In case it's helpful in evaluating the proposal, my underlying goal here is to make a set of values that are all comparable and all implement a specific interface, but that have different dynamic types. This is actually part of an implementation of a directed acyclic graph (
I'm not planning to pursue this any further for the moment, since it seems unlikely I will be able to achieve this goal with Go 1.18. I hope these details are helpful in evaluating the proposal. Next day update: FWIW, I was able to work around this for now by separating the idea of a graph node (a pointer to a specific struct type) from the idea of an action, so that a graph node has an action, rather than is an action. For my purposes that seems to be sufficient for the moment, although this wrapping graph node struct isn't really adding anything right now except something concrete to save a pointer to. type GraphNode struct {
Action Action
}
type Action interface {
// ...
}
type ActionGraph = graph.Graph[*GraphNode] |
Is it possible to disallow comparisons with incomparable interfaces, to avoid runtime panics during comparisons? (looks hard). |
I would think that disallowing comparisons with interfaces that aren't statically incomparable would be a breaking change at this point, since it's always been possible so far to carefully compare interface values and just make sure that you don't write any implementations that aren't themselves comparable. 🤔 I have several examples in the codebases I maintain in my dayjob which would immediately fail compile under that rule, even though in practice they can never panic at runtime today. It does seem like a shame, since this seems like a clear win if this were a green field problem, but I think I'd rather have the ability to declare that all values of a particular interface type are guaranteed never to panic on comparison, even if it remains possible to accept the risk and try comparing interface values of a non-statically-guaranteed-comparable interface type. |
@go101 We can't change the existing language, so it will continue to be possible to compare values of interface type which can possibly panic. That said, if we adopt this proposal, then programs that care can convert their interface types to be |
I think this is a good change, but mostly because I see it as a step on the path to making any constraint useable as a normal interface value, which in turn opens the door to tagged union types. I know that using constraints as interface values has some wrinkles to work out (given |
I fell at (nearly) the first hurdle at trying to make a generic |
This proposal has been added to the active column of the proposals project |
TIL. This surprised me, I missed that discussion. IMO that was a mistake and the answer to this problem is "interface types are comparable, so they should satisfy Personally, I'm against this proposal for the reasons I mentioned when I last saw this idea being floated. Namely that embedding
So you'll need both versions, in practice, for every interface. That's a color.
That seems an unreasonable amount of effort, to me. I understand why the decision was made, to have interface-types not satisfy Deciding that interface-types don't satisfy |
Is this proposal significantly different from the idea I floated here? #49587 (comment) In that comment, I was under the impression that interface types would satisfy the Although I have a soft spot for this idea, I don't think it fits well with the language as is. As a general rule, I think that if one is allowed to use That said, I would support a change to the language that's compatible with the goal expressed in the subject of this proposal: I think that values should be permitted to have type "comparable". I propose a much simpler rule: when used in a non-constraint context, Given:
it would be possible to write The main benefit of doing this is that it would be possible to use a comparable constraint interface
For the record, I disagree with this. It's an important part of some interfaces that their implementations are comparable - that's part of the contract. |
@rogpeppe I agree that for some interfaces it's important. But for most it's counterproductive. By making interface types not satisfy That's kind of my point.
But I'd argue, we still need this duplication under the proposal, if we want to be able to use e.g. // SetAny is a Set[T], which doesn't statically guarantee its values are comparable, for cases where
// you want to use an interface type which doesn't categorically require comparable, but where you know
// the values revelant for your case are. It panics at runtime, if you try adding non-comparable values to it.
type SetAny[T any] struct { m map[comparable]bool }
func (s SetAny[T]) Contains(v T) bool { return s.m[v.(comparable)] }
func (s SetAny[T]) Add(v T) { s.m[v.(comparable)] = true }
// etc I also think that we need to keep current usage in mind. |
@rogpeppe Let me clarify: In my comment above, when I said "X is bad", i didn't mean "it is categorically bad, for all types", but "it has downsides". Both embedding and not embedding |
I proposed that:
For the record, that would imply that the following would be OK:
An alternative might be to consider
|
Is there really a problem appart from reflect.Type ? How often do people use any potentially non-comparable interface values in Set and why would they not type assert to Embedding |
The majority of interface values should be non-comparable. There are only two reasons I can think of, to embed Those are relatively rare. Most interface usages are to abstract over behavior, which shouldn't care about the dynamic type of its value.
Of course, they can do that. That's what I said here, under "we still need this duplication". Of course the author of the And, of course, the even larger problem of using struct-types with interface-fields still persists after that. That's not something that can even be solved with a type-assertion. |
I am still mulling over the implications, but it seems to me that the migration for interfaces to Like With However, I'm not sure that the |
It seems to me that the virality problems could be at least partially addressed by defining “comparable” as a parameterized type rather than (just) an interface type. Its behavior would be roughly analogous to the Here I'll describe that type as
|
At the very least, type MapOf[K comparable, V any] map[K]V
func assign(m MapOf[Comparable[any], any], k, v interface{}) {
kc := Comparable[any](v)
m[kc] = v
} The ergonomics still aren't great, but at least the conversion becomes possible to write at all. |
What if we just don't apply On the other hand, it would be possible to offer a wrapping interface to maps that uses the
Edit: Perhaps that the current constraint is not yet well defined and does not equal to Edit2: needs something more to deal with struct fields... |
I guess, to summarize my comments above: defining Consider the implementation of the type Set[E any] map[comparable]bool
func Union[E any](s1, s2 Set[E]) Set[E] {
result := make(Set[E])
for k, ok := range s1 {
if ok {
result[k.(comparable)] = true
}
}
for k, ok := range s2 {
if ok {
result[k.(comparable)] = true
}
}
} Now consider what happens if we instantiate above with the type: type S struct {
I any
} As currently proposed in #51338 (comment), the above implementation of The problem is that the type-assertion rule is too weak (emphasis mine):
In order for the
But if we make that revision, now we have a bit of a paradox: |
What if a struct required all its fields to implement Isn't that the only way to be sure at compile-time that a composite type implements |
Because |
ISTM this isn't substantially different than the proposal, except that the proposal spells the conversion as a type-assertion. i.e. it spells |
Maybe the language spec should just change to define comparability for slices, maps, and functions?
Then all types in Go are "comparable". Consequently I haven't thought through any negative implications this might have, but it sidesteps the issues mentioned throughout this thread about the two flavors of "comparable". |
@rittneje Non-comparable types where made non-comparable for a reason. I don't think generics change the equation on that significantly. |
Based upon expanding the `StringSet` type. Note that we cannot substitute uses of `map[T]struct{}` where `T` is an interface because interface types are [not comparable](golang/go#51338), even though the `map[T]struct{}` version works. `AreEqual` is a standalone function at the moment because making it a method [crashes the compiler](golang/go#51840).
Thanks for all the discussion. We've tried to address the most pressing issue on #52474. If that proposal is accepted, I will most likely close this one and recreate it in simpler form. |
Change https://go.dev/cl/401874 mentions this issue: |
See #52614 for another take on this problem. |
FWIW at this point, after all the discussion, I think I've fully come around to this proposal. I think my concerns are largely valid, but a) they happen relatively rarely, b) we might find ways to somewhat alleviate them and c) overall, after some transition pains, I think this leads to the most consistent language in the long term. So, at least my personal opinion on this has changed. |
It happens I've got here because I stumble on this issue while trying to refactor a simple code with generics, I cannot instantiate with a comparable type
throw me an error
This is the original code, been trying to replace the type check that prevents panic with a comparable constraint type and let the check happen on the compiler itself
|
Is it possible for a Go team member to share the team's current thinking on this situ? I'm wondering if you have an impression of a sensible path forward, or what would kind of consensus/changes would be needed in order to move forward? I realise it's probably too late to get this resolved in 1.19, but it'd be great if we can get the info required to make sure we can get it figured out in time for 1.20. |
It's fair to say that we're still deliberating this issue. One option might be to go back to the situation pre-Go 1.18 where ordinary interfaces did satisfy comparable, while non-comparable constraints did not. There's probably different ways we can get there. One way might be what @Merovius lined out in this comment a while back. Another approach might be to more closely match the definition to what the compiler actually implements with the existing restrictions, which is to treat method and type sets somewhat independently, and carve out a special case for non-type parameter interfaces. This may be a bit of a departure from what the spec says right now (interfaces are "just" type sets), but it wouldn't affect existing code. Not much may happen in the next month or so as Gophers may be on vacation. But we do think it's important to solve this rather sooner than later, ideally for 1.20. |
Retracting in favor of #56548. |
The two issues are two different approaches to solving the same problem. Of course, we could also permit values to have type |
As part of adding generics, Go 1.18 introduces a new predeclared interface type
comparable
. That interface type is implemented by any non-interface type that is comparable, and by any interface type that is or embedscomparable
. Comparable non-interface types are numeric, string, boolean, pointer, and channel types, structs all of whose field types are comparable, and arrays whose element types are comparable. Slice, map, and function types are not comparable.In Go, interface types are comparable in the sense that they can be compared with the
==
and!=
operators. However, interface types do not in general implement the predeclared interface typecomparable
. An interface type only implementscomparable
if it is or embedscomparable
.Developing this distinction between the predeclared type
comparable
and the general language notion of comparable has been confusing; see #50646. The distinction makes it hard to write certain kinds of generic code; see #51257.For a specific example, you can today write a generic
Set
type of some specific (comparable) element type and write functions that work on sets of any element type:But there is no way today to instantiate this
Set
type to create a general set that works for any (comparable) value. That is, you can't writeSet[any]
, becauseany
does not satisfy the constraintcomparable
. You can get a very similar effect by writingmap[any]bool
, but then all the functions likeUnion
have to be written anew for this new version.We can reduce this kind of problem by permitting
comparable
to be an ordinary type. It then becomes possible to writeSet[comparable]
.As an ordinary type,
comparable
would be an interface type that is implemented by any comparable type.comparable
.comparable
could be assigned to a variable of typecomparable
.comparable
, as inx.(comparable)
, would succeed if the dynamic type ofx
is a comparable type.case comparable
.The text was updated successfully, but these errors were encountered: