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
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
.- (Note: not every type in the type-set of
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.)
- (Note: not every type in the type-set of
-
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:
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
comparable
interface. - 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
comparable
type parameters to be instantiated with existing ordinary interface types. - Also unlike that proposal, under this proposal the
==
operator applied to two variables ofcomparable
interface 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
comparable
interface 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