Repository navigation
proposal: spec: allow values of type comparable #52624
Description
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 implements comparable is assignable to comparable interfaces (see below).
A variable of a parameter type is comparable only if all of the types that implement its constraint are comparable.
func eq1[T any](a, b T) bool {
return a == b // Compile error: type T is not necessarily comparable.
}
func eq2[T comparable](a, b T) bool {
return a == b // Ok, but may panic at run time if values are not comparable.
}
func eq3[T int | []byte](a, b T) bool {
return a == b // Compile error: type T is not necessarily comparable.
}
func eq4[T int | string](a, b T) bool {
return a == b // Ok; cannot panic at run time.
}If T is an interface type that either is or embeds comparable, a variable of type T can hold only comparable run-time values.
-
An ordinary assignment of a value
xto a variable of typeTis allowed only ifxis 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.- (Note: not every type in the type-set of
Tis necessarily assignable toT! Types that may panic — such as non-comparableinterface types and structs with non-comparableinterface fields — require a type-assertion instead.)
- (Note: not every type in the type-set of
-
A type-assertion of a value
xto a variable of typeTis allowed ifxis of any comparable type (even a concrete type). The type-assertion succeeds only ifx == xwould not panic.
That is:
var i any = func() {}
x, ok := i.(comparable) // Compiles and does not panic: x == nil and ok == false.
y := i.(comparable) // Compiles, but panics at run-time because i is not comparable.
var z comparable = i // Does not compile: i is not necessarily comparable.
var w comparable = math.NaN() // Compiles: a float64 can always be compared without panicking.Discussion
Like #52509 and #52614, this proposal allows comparable type parameters to be instantiated with ordinary interface types.
- Unlike those proposals, this one also provides run-time semantics for the
comparableinterface. - Unlike #52614, this proposal does not allow comparisons of type parameters whose type sets include incomparable types.
Like #51338, this proposal allows the use of comparable as an ordinary interface type.
- Unlike that proposal, this one allows
comparabletype parameters to be instantiated with existing ordinary interface types. - Also unlike that proposal, under this proposal the
==operator applied to two variables ofcomparableinterface types cannot panic at run-time.- Note that, because today we have no way to constrain type parameters to be interface types, comparing variables of type parameters constrained by
comparableinterface types can panic at run-time.
- Note that, because today we have no way to constrain type parameters to be interface types, comparing variables of type parameters constrained by
Notably, this proposal allows panics from the == operator to be avoided (not just recovered!) using a simple type-assertion:
func Equal(a, b any) (eq bool, err error) {
if _, ok := a.(comparable); !ok {
return false, ErrIncomparable
}
if _, ok := b.(comparable); !ok {
return false, ErrIncomparable
}
return a == b, nil
}Examples
From #52509 (comment):
func Eq[T comparable](a, b T) bool { return a == b }
func F() {
Eq(0, 0) // ok
Eq([]int{}, []int{}) // compilation error: []int is not comparable
Eq(any([]int{}), any([]int{})) // compiles OK, panics at run time. ('any' is comparable.)
Eq[any]([]int{}, []int{}) // compiles OK, panics at run time
Eq[any]([]int{}, 0) // compiles OK, runs OK, returns false
}
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
}From #52614, with further types added, as a variable:
interface comparable may panic
any yes yes
comparable yes no
interface{ m() } yes yes
interface{ m(); comparable } yes no
interface{ ~int } yes no
interface{ ~struct{ f any } } yes yes
interface{ ~[]byte } no
interface{ ~string | ~[]byte } yes yes
But, as a constraint:
constraint comparable may panic
any no
interface{ m() } no
interface{ ~int } yes no
interface{ ~struct{ f any } } yes yes
interface{ ~[]byte } no
interface{ ~string | ~[]byte } no
Activity
Merovius commentedon Apr 29, 2022
Would this compile?
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
comparableto mean. YMMV, of course.[edit2] to clarify: By "what I would intuit
comparableto mean", I mean when used as an interface type, as well as when used as a constraint.bcmills commentedon Apr 29, 2022
@Merovius, I think the
P(v).M()line is always a compile error today, regardless of comparability (https://go.dev/play/p/wkSDX6EATJu). But that's orthogonal to this proposal. 😅Merovius commentedon Apr 29, 2022
Whoops. Edited sneakily.
bcmills commentedon Apr 29, 2022
Ok, then here we go!
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).)
neild commentedon Apr 29, 2022
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.
bcmills commentedon Apr 29, 2022
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. 😅
bcmills commentedon Apr 29, 2022
@neild, updated with more precise language based on the current spec.
bcmills commentedon Apr 29, 2022
(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.)
neild commentedon Apr 29, 2022
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
error(for example) is not in the type set ofany, because you can't store a value of typeerrorin a variable of typeany.You can, however, assign a value of type
errorto a variable of typeany, becauseerrorimplementsany.griesemer commentedon Apr 29, 2022
So in this example given above
the two calls of
EqareEq[T](a, b)andEq[any](a, b)(after type inference). The type set ofTis the type set of its constraint which isany. So the instantiation ofEq[T]is not permitted (whileEq[any]is) becauseTis a type parameter and not an ordinary interface (even though the type sets seem identical)? Do I have that right?bcmills commentedon Apr 29, 2022
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
Tis indeed equal to the type set ofany.More or less, yes: a type parameter is not necessarily an interface type.
(
Eq[T]is not permitted becauseTis not guaranteed to implementcomparable—Tand its constraint are two different entities!)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.)
griesemer commentedon Apr 29, 2022
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.
20 remaining items
jimmyfrasche commentedon May 5, 2022
It's awkward looking written as that but it looks less awkward if you write it as
F∈typeset(any)any∈typeset(comparable)F∉typeset(comparable)as it makes it clear that the type and the typeset it generates are different kinds of things
It also makes it clearer why
interface { A | B }∉typeset(interface { A | B })={A,B}.bcmills commentedon May 6, 2022
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
comparablefor the “a literal type” in that sentence, and you have a perfect analogy (https://go.dev/play/p/a0NmWliZNyG):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.
bcmills commentedon May 6, 2022
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:
a) x is assignable to i if typeset(x) ⊆ typeset(i) and either every value in type(x) is comparable or i does not embed
comparableIn addition, I think both proposals need an explicit rule for type-assertions. For this one:
a) x.(
I) succeeds ifT⊆ typeset(I) (whereTis the dynamic type of x) and either the value of x is comparable orIdoes not embed comparable[-]proposal: the `comparable` interface represents the comparable subset of run-time values[/-][+]proposal: spec: allow values of type comparable[/+]ianlancetaylor commentedon Nov 4, 2022
We have proposed #56548 as an alternative to this approach. If we adopt #56548 we will not be adopting this proposal. Putting this one on hold pending a decision on #56548.
qiulaidongfeng commentedon Jan 18, 2024
Look like this issue can be close.
griesemer commentedon Jan 18, 2024
#56548 was implemented, so we don't need this anymore.