Description
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 by int
and string
and by composite types like struct { f int }
or [10]string
. On the other hand, it is not satisfied by types like any
or interface { String() }
or struct { 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 interface I
if "the type set of T
is a subset of the type set of I
." By this definition the type any
does not implement comparable
: there are many elements in the type set of any
that are not in the type set of comparable
. More generally, no basic interface implements comparable
. Some general interfaces, such as interface { ~int }
, implement comparable
; 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 implement comparable
.
For a non-interface type, the rule is different. A non-interface type T
implements an interface I
if it "is an element of the type set of I
." For a language-defined type like comparable
, the language defines that type set. The current definition says that comparable
"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 that comparable
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:
type Value struct {
Annotation string
Val any
}
// Various methods on Value.
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 type map[annotated.Value]bool
. That works because the type annotated.Value
is comparable according to the language definition. However, annotated.Value
does not implement comparable
, because the type of the element Val
is an interface type that does not implement comparable
. That means that code like this does not work:
type Set[Elem comparable] map[Elem]bool
var ValSet Set[annotated.Value]
Since annotated.Value
does not implement comparable
, 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 restricts Val
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 restrict Val
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:
- the type set of
comparable
is all non-interface types that are comparable per the language spec
As before:
- an interface type implements
comparable
if the type set of the interface type is a subset of the type set ofcomparable
- a non-interface type implements
comparable
if it is a member of the type set ofcomparable
For example, by this definition, annotated.Value
implements comparable
, 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 the fmt.Stringer
type but only permitting comparable types. If we adopt #51338, then people can get this type by writing interface { 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.