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:
As before:
- an interface type implements
comparable if the type set of the interface type is a subset of the type set of comparable
- a non-interface type implements
comparable if it is a member of the type set of comparable
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.
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 byintandstringand by composite types likestruct { f int }or[10]string. On the other hand, it is not satisfied by types likeanyorinterface { String() }orstruct { 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
comparableconstraint, there are two cases to consider.For an interface type, the rule in the spec is simple: an interface type
Timplements an interfaceIif "the type set ofTis a subset of the type set ofI." By this definition the typeanydoes not implementcomparable: there are many elements in the type set ofanythat are not in the type set ofcomparable. More generally, no basic interface implementscomparable. Some general interfaces, such asinterface { ~int }, implementcomparable; 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 implementcomparable.For a non-interface type, the rule is different. A non-interface type
Timplements an interfaceIif it "is an element of the type set ofI." For a language-defined type likecomparable, the language defines that type set. The current definition says thatcomparable"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 thatcomparableshould 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:
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
Valfield. It's fine for package p to use the typemap[annotated.Value]bool. That works because the typeannotated.Valueis comparable according to the language definition. However,annotated.Valuedoes not implementcomparable, because the type of the elementValis an interface type that does not implementcomparable. That means that code like this does not work:Since
annotated.Valuedoes not implementcomparable, 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.Valuethat restrictsValto 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 restrictValto 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:
comparableis all non-interface types that are comparable per the language specAs before:
comparableif the type set of the interface type is a subset of the type set ofcomparablecomparableif it is a member of the type set ofcomparableFor example, by this definition,
annotated.Valueimplementscomparable, 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 thefmt.Stringertype but only permitting comparable types. If we adopt #51338, then people can get this type by writinginterface { 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.