-
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: allow values of type comparable #52624
Comments
Would this compile? type X[T any] interface {
*T
M()
}
func F[T comparable, P X[T]](v T) {
P(&v).M()
fmt.Println(v == v)
}
type S struct { A any }
func (s *S) M() { s.A = func() {} }
func main() {
F(S{})
} I don't see why it would be forbidden. It would then probably panic. Correct? [edit] Ah, I see, this falls under
TBH I feel that's a downside compared to #51338. In general, I feel that #51338 provides overall better semantics - the semantics described there are much closer to what I would intuit [edit2] to clarify: By "what I would intuit |
@Merovius, I think the |
Whoops. Edited sneakily. |
Ok, then here we go! type X[T any] interface {
*T // Since the type set of X[T] is always a pointer type, a type constrained by X[T] is comparable.
M()
}
func F[T comparable, P X[T]](v T) {
P(&v).M()
fmt.Println(v == v) // This is allowed (because T is comparable), but may panic at run-time.
// (And that fact is completely independent of both P and X.)
}
// S is comparable: it is a struct type whose fields are comparable.
// (Field A is comparable because the type-set of 'any' includes at least one comparable type.)
type S struct { A any }
func (s *S) M() { s.A = func() {} }
func main() {
F(S{}) // This panics at run-time.
// (The call to M() causes the resulting value to become incomparable, and then it is compared.)
} Note that the equivalent non-generic code also compiles successfully and panics at run-time: https://go.dev/play/p/WZtngo905Xj In my opinion that is a good thing, because it allows one to reason about generic code by thinking through (and experimenting with) non-generic equivalents. (Compare #52614 (comment).) |
This may seem like a nitpick, but it's significant: Currently, an interface's type set never contains an interface type. I think what you want to say is that all comparable types implement the comparable interface, not that they are in its type set. "A is in the type set of B" means that B can store an A, including A's type. "A implements B" is a weaker assertion. |
Indeed. I think my understanding of type sets and “implements” is still a bit of a holdover from earlier discussions in which those two concepts were identical. I don't fully understand why they're not. 😅 |
@neild, updated with more precise language based on the current spec. |
(On further reading I think the current spec is more-or-less incoherent with the interpretation of union terms as run-time interface types — but that's orthogonal to this proposal.) |
A variable of interface type can store a value of any type that is in the type set of the interface. A variable of interface type can't store a value of interface type.
Therefore You can, however, assign a value of type |
So in this example given above func G[T any](a, b T) {
Eq(a, b) // compilation error: T is not guaranteed to be comparable
Eq[any](a, b) // OK, may or may not panic at run time
} the two calls of |
I honestly no longer have a coherent understanding of type-sets. (Something changed in between the initial proposal and the wording in the current spec and it doesn't click at all for me any more.) From @neild's explanation above, though, I would expect that the type set of
More or less, yes: a type parameter is not necessarily an interface type. I note that the spec currently says that “[f]or a type parameter [the parameter's underlying type] is the underlying type of its type constraint, which is always an interface.” I think that sentence is erroneous: the underlying type of a type parameter cannot be an interface type, because nothing constrains the parameter to be instantiated with an interface type. (It is not obvious to me whether the actual implementation in Go 1.18 contains the same error.) |
Note that we're careful to talk about "non-type parameter interfaces" in the spec. A type parameter is an interface with a "fixed" dynamic type if you will, which is why it has special properties. |
I haven't been keeping up with the spec changes, but that seems to weaken the notion of “interface types” to be nearly meaningless. A type parameter does not behave at all like an interface type with a fixed dynamic type: its zero-value is not the nil interface value, it doesn't present as an interface value when observed with |
I think that the way to grok this for me is to come back to the spec. This set of permissible values is much larger that the one defined by However type sets do not directly matter here. They do not even include interface types (for good reasons). |
Assignment to an interface actually changes type sets and we are ignoring that. Given var f func()
var x any = f the comparison On the other hand, when we talk about the constraint (type parameter type) If we now define
Again, the type sets for the two forms of |
I think I may understand that comment after reading it a couple of times, but I find the whole set of ideas rather confusing. I would like to believe that If I understand this proposal correctly (I may not) whether a type argument satisfies the type parameter constraint (Separately, this proposal says "unlike [#51338], under this proposal the == operator applied to two variables of comparable interface types cannot panic at run-time." Just a note that the same is true for #51338: it does not permit a panic when using |
We could imagine that we have two different equality operators. One is This seems straightforward for non-generic Go code. Various instances of In a generic function, a constraint with a type set of only comparable types permits the use of This leads us to the following: type Set[T comparable] map[T]bool // OK; map uses ==
type Set[T any] map[T]bool // Does not compile
type Set[T any] mapi[T] bool // OK; mapi uses =i=
func Eq1[T comparable](a, b T) bool { return a == b } // OK
func Eq2[T any](a, b T) bool { return a == b } // Does not compile
func Eq3[T any](a, b T) bool { return a =i= b } // OK Back in reality, we can't change existing Go code to use But something we could do is to change the constraint. The constraint Once we do that we can go back to writing Then we just need a name for that constraint. I guess this gives us something similar to #52531. |
Likewise. But we do have the odd situation that The alternative view is what we have now. If we believe There's another approach: |
Another approach we could take (which I suspect that someone has already suggested) is to introduce a notation specific to constraints that means "this constraint may only be instantiated by an interface type". We permit In fact there is any easy way to write such a constraint: Then we can write type Set[T interface | comparable] map[T]bool I don't really like this idea but semantically I think it addresses all problems. |
Sure, but really the type parameter constraint |
I don't follow what you mean by the implements relationship being explained in terms of type sets. Under the current language of the spec, a "type set" is the set of types that can be stored in a variable of interface type. This explicitly excludes other interface types, because an interface variable cannot store an interface value. The implements relationship seems distinct from type sets. |
Oh I see! Yes if we look at comparison in a fine-grained way. We could also look at it coarsely as a part of an operation specific to interface types. (Let me try to mirror you with this other perspective to see where it leads)
Yes. The set of permissible type arguments; which in this case, can be any type.
Here I think our perspectives would diverge slightly. Per spec, we don't know if If we said that package main
import "fmt"
func G[T comparable](a T) T {
return a
}
func main() {
fmt.Println(G[func()](nil))
}
Since type constraints define sets, we could also express that in terms of set non-inclusion.
Well, we defined above the type constraint So That would perhaps allow us an additional rule such that "a type is in the set of permissible types defined by an interface constraint iff it implements it (i.e. type set inclusion)." I don't know if that rule would hold. So far, I had been reasoning in terms of sets of permissible types only. |
I find that interpretation extremely confusing — it is defining type sets in terms of operators instead of the other way around. The mistake, I think, is in treating interface types as merely “sets of types” rather than types in and of themselves. But they cannot be merely sets of types: I can have, say, a type that is a pointer to an interface type ( When we acknowledge that interface types are types, then we can also see that the So I see a serious problem that I see with the current formulation of type sets, which is that they explicitly fail to include an entire subset of the types in the language (namely, the interface types). (I thought some more about the implications of this problem, and filed #52628 for one of its ramifications.) We see that in the current definition of “implements”: it is not one uniform rule about types sets (as I would expect it to be), but rather a pair of rules — one for type sets, and a different one for interface types. A more uniform rule would include interface types in type sets, with two properties:
But, I think this is all basically a digression: it is relevant here only in the sense that it shows that type parameters should not be treated as just fancy interface types. |
I do not think that holds. In that proposal:
The type type SemiComparable struct {
X interface{}
} I do not see anything in proposal #51338 that prevents that panic. In contrast, the special case for assignability in this proposal prevents a value of type |
I agree. As far as I can tell, this proposal does keep
Again I agree. And as far as I can tell this proposal does keep ”implements” as consistent with type sets as it is today. However, since interface types for some reason are not included in their own type sets today, I think “implements” is already wildly divergent from type sets. (FWIW, I think that could be corrected by expanding the type set for interface types to include all interface types that implement it, but we would need to take care to avoid a circular definition.) At any rate: the main point of flexion in this proposal is assignability, not “implements” or type sets. Under this proposal, the set of types that implement the That is: this proposal breaks the relationship between “implements” and “is assignable to”, not the relationship between “implements” and type-sets. |
@neild On implementing an interface the spec says: A type T implements an interface I if
A value of type T implements an interface if T implements the interface. That is, the "implements" relation is very much defined in terms of type sets. |
We can't prevent that panic in and off itself, as the current language allows it and we can't break backwards compatibility. But with #51338, generic code constrained on At least as I understand it. I don't think it is made explicit in the proposal text, but my understanding is, that a struct type would implement You say
I don't think you that is unlike #51338 at all. I don't think |
@bcmills The problem lies with |
@bcmills Regarding your #52624 (comment) on interfaces as type sets: Interfaces are types in the language, there's no question about that. But an interface variable never stores an interface, only concrete types. Thus, viewing an interface as a set of concrete types is fitting and as far as we have seen consistent (ignoring the debate around comparable for the moment). The fact that an interface cannot be in an interface's type set is not hurting this view in any way. Your Regarding comparability: The correct answer may well be that for comparability of (variable) interfaces we don't look at the type set but the interface type itself. This is the "other approach" I have mentioned in #52624 (comment). For instance, for Unless I am mistaken, all proposals so far have either come up with a precise mechanism but fallen short of what we want to express in code, or outlined everything we want to express in code but failed to explain the precise mechanism. |
I think that most proposals would need to consider the following issues: If we define
|
@atdiar, that's a very good point about subtyping. I think there is something of an analogy with union interface types. Today, “implements” is the basis of type-parameter instantiation. Per https://go.dev/ref/spec#Instantiations:
#45346 suggested “permitting constraints as ordinary interface types” as a next step, and the various proposals about So, consider this program, which is valid in Go 1.18: type Plusser interface {
~int | ~string
}
func Add[T Plusser](a, b T) T { return a + b } If Note that an ordinary interface type must implement itself. This program surely must be valid: func Println[T fmt.Stringer](x T) {
fmt.Println(x.String())
}
func main() {
buf := new(strings.Builder)
buf.WriteString("Hello, world!")
var s fmt.Stringer = buf
Println(s)
} As it turns out, So, indeed, we do have the case that “ |
It's awkward looking written as that but it looks less awkward if you write it as
as it makes it clear that the type and the typeset it generates are different kinds of things It also makes it clearer why |
Yeah. On further consideration, I think the fact that this would break subtyping isn't actually that big a deal in and of itself, considering that Go already doesn't have a formal concept of subtypes (compare https://github.com/bcmills/go2go/blob/master/subtypes.md). The thing that Go uses in place of subtypes is assignability, and there we already have some kind of analogous cases: a named type is assignable to the corresponding literal type, and a value of a literal type is assignable to any corresponding named type, but named types are not assignable to each other. Substitute var f F // type F struct{}
var a A // type A = struct{}
var b B // type B struct{}
a = f // F is assignable to A
b = a // A is assignable to B
b = f // F is not assignable to B But, it's true that this would break the existing relationship between “implements” and “assignable to”, and it would break the existing correspondence between “is or implements” and subtyping. So probably we need a different approach. |
Looking at this proposal through the lens of #52509 (comment), I think we could use almost exactly those rules to describe this proposal. The difference would be in the assignability rule. #52509 allows:
but this proposal would have an additional caveat:
In addition, I think both proposals need an explicit rule for type-assertions. For this one:
|
comparable
interface represents the comparable subset of run-time values
Look like this issue can be close. |
#56548 was implemented, so we don't need this anymore. |
Introduction
Since we're proposing
comparable
semantics, I'd like to throw this one into the ring.This is a proposal for the approach mentioned in #51338 (comment). It is a possible alternative to proposals #52509 and #52614; please see those proposals for introduction and background.
Proposal
An interface type is comparable if its type-set includes at least one comparable type. Interfaces whose type-sets include only incomparable types — such as
interface { func() | func() string }
— are not comparable. (Comparability for all other types remains as it was defined in Go 1.18.)Every comparable type implements the
comparable
interface. (Thus,comparable
is itself comparable.) However, not every type that implementscomparable
is assignable tocomparable
interfaces (see below).A variable of a parameter type is comparable only if all of the types that implement its constraint are comparable.
If
T
is an interface type that either is or embedscomparable
, a variable of typeT
can hold only comparable run-time values.An ordinary assignment of a value
x
to a variable of typeT
is allowed only ifx
is of a type whose values can always be compared without panicking, such as an integer type or an interface type that itself is or embedscomparable
.T
is necessarily assignable toT
! Types that may panic — such as non-comparable
interface types and structs with non-comparable
interface fields — require a type-assertion instead.)A type-assertion of a value
x
to a variable of typeT
is allowed ifx
is of any comparable type (even a concrete type). The type-assertion succeeds only ifx == x
would not panic.That is:
Discussion
Like #52509 and #52614, this proposal allows
comparable
type parameters to be instantiated with ordinary interface types.comparable
interface.Like #51338, this proposal allows the use of
comparable
as an ordinary interface type.comparable
type parameters to be instantiated with existing ordinary interface types.==
operator applied to two variables ofcomparable
interface types cannot panic at run-time.comparable
interface types can panic at run-time.Notably, this proposal allows panics from the
==
operator to be avoided (not just recovered!) using a simple type-assertion:Examples
From #52509 (comment):
From #52614, with further types added, as a variable:
But, as a constraint:
The text was updated successfully, but these errors were encountered: