Description
Introduction
The predeclared type constraint comparable
, introduced with Go 1.18, is a (magic) interface describing the set of types for which ==
is expected to work without panic. The introduction of comparable
led to considerable discussion (see background in #52474 for details). It also led to confusion because the set of types described by comparable
does not match the types that are considered comparable per the Go spec.
Here's the current list of issues related to this discussion:
- spec: document/explain which interfaces implement
comparable
#50646 - spec:
any
no longer implementscomparable
#51257 - proposal: spec: permit values to have type "comparable" #51338
- proposal: spec: permit non-interface types that support == to satisfy the comparable constraint #52474
- proposal: spec: allow interface types to instantiate
comparable
type parameters #52509 - proposal: spec: add new constraint kind satisfied by types that support == (including interface types) #52531
The goal of these proposals is to address the perceived shortcomings of comparable
by changing its definition or by separating the notion of interfaces and type sets.
So far none of these proposals (if still open) have gained significant traction, and none of them directly address the core of the comparable
problem: in Go ordinary interfaces are always comparable, i.e., they support ==
and !=
independently of whether the dynamic type of the interface is comparable. We cannot change this without breaking backward-compatibility.
Instead we propose to embrace this property of interfaces.
Proposal
The underlying type of a type parameter is its type constraint interface; i.e., a type parameter is an interface (albeit with a "fixed" dynamic type which is given when the type parameter is instantiated). Because type parameters are interfaces, we propose:
Type parameters are comparable unless the type parameter's type set contains only non-comparable types.
This is the entire proposal.
Discussion
The reason for having comparable
in the first place is to be able to statically express that ==
is expected to work and that it won't panic. If this proposal is accepted, ==
will be supported on type parameters unless the type set contains only non-comparable types. We will also lose the guarantee that ==
won't panic (if ==
is supported in the first place). We may still keep comparable
, but more on that below.
This proposal hinges on the premise that losing the static "no-panic" guarantee is not as severe a loss as it might appear at first. We believe this could be true for the following reasons:
-
We are well-accustomed to the fact that
==
on ordinary interface types might panic. In code, we tend to address the comparability requirement through documentation; we suggest that we continue to use documentation for this. -
If a type parameter is instantiated with a non-comparable type and
==
is expected to work, upon invocation the generic code is likely to panic right away. This contrasts favorably to the situation with ordinary interfaces where a panic may occur for some of the dynamic values but not all of them. In other words, making a comparability mistake in generic code would be detected quickly, probably in the first test run. -
Better yet, we don't have to rely entirely on dynamic type safety: it should be straight-forward to introduce a
vet
check that reports when a type parameter for which we expect==
to work is instantiated with a type that is not comparable. Such a check would provide the equivalent of a static compile-time check, and virtually eliminate the risk of==
-related panics.
With this proposal unfortunate restrictions caused by the use of comparable
can be avoided. The ==
operator will simply be available to all type parameters unless their type sets contain only non-comparable types (it makes sense to exclude such type sets because we know with certainty that ==
will always panic for such type parameters). Examples:
interface comparable may panic
any yes yes
interface{ m() } yes yes
interface{ ~int } yes no
interface{ ~struct{ f any } } yes yes
interface{ ~[]byte } no n/a
interface{ ~string | ~[]byte } yes yes
This proposal also opens the door to more flexible (if perhaps esoteric) generic code that relies on ==
to work for some type instantiations but not for others, something that can be readily expressed through control flow but which is much harder (or impossible) to encode through types.
We still have the option to keep comparable
as the "umbrella" set of types which are comparable without panic. Or we could decide to remove it because using it may preclude some uses of generic code (e.g., see #51338 (comment)). Keeping it will also require a programmer to always make the decision whether or not to use it. To remove it we could make use of the provision in the Go 1 compatibility guarantee:
If it becomes necessary to address an inconsistency or incompleteness in the specification, resolving the issue could affect the meaning or legality of existing programs. We reserve the right to address such issues, including updating the implementations.
Eliminating comparable
would simplify the language and probably eliminate some confusion. The decision whether to keep or remove it is independent of this proposal.
History and credits
We briefly toyed with a simpler form of this idea (type parameters should always be comparable) as a potential solution to the comparable
problem shortly before the 1.18 release. At that time we dismissed making all type parameters comparable (and eliminating the predeclared type comparable
) as too radical. The resulting loss of static type safety around ==
in generic code seemed unacceptable.
We are aware of at least one other person, Conrad Irwin, who independently suggested that all type parameters should be comparable in #52509 (comment).