Skip to content

proposal: spec: allow values of type comparable #52624

Closed as not planned
Closed as not planned

Description

@bcmills

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 type T is allowed only if x 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 embeds comparable.

    • (Note: not every type in the type-set of T is necessarily assignable to T! Types that may panic — such as non-comparable interface types and structs with non-comparable interface fields — require a type-assertion instead.)
  • A type-assertion of a value x to a variable of type T is allowed if x is of any comparable type (even a concrete type). The type-assertion succeeds only if x == 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 of comparable 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.

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

  1. added this to the Proposal milestone on Apr 29, 2022
  2. Merovius commented on Apr 29, 2022

    @Merovius
    Contributor

    Would this compile?

    type X[T any] interface {
        *T
        M()
    }
    
    func F[T comparable, P X[T]](v T) {
        P(&v).M()
        fmt.Println(v == v)
    }
    
    type S struct { A any }
    
    func (s *S) M() { s.A = func() {} }
    
    func main() {
        F(S{})
    }

    I don't see why it would be forbidden. It would then probably panic. Correct?

    [edit] Ah, I see, this falls under

    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.

    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 comparable to mean. YMMV, of course.

    [edit2] to clarify: By "what I would intuit comparable to mean", I mean when used as an interface type, as well as when used as a constraint.

  3. bcmills commented on Apr 29, 2022

    @bcmills
    ContributorAuthor

    @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. 😅

  4. Merovius commented on Apr 29, 2022

    @Merovius
    Contributor

    Whoops. Edited sneakily.

  5. bcmills commented on Apr 29, 2022

    @bcmills
    ContributorAuthor

    Ok, then here we go!

    type X[T any] interface {
        *T // Since the type set of X[T] is always a pointer type, a type constrained by X[T] is comparable.
        M()
    }
    
    func F[T comparable, P X[T]](v T) {
        P(&v).M()
        fmt.Println(v == v)  // This is allowed (because T is comparable), but may panic at run-time.
                             // (And that fact is completely independent of both P and X.)
    }
    
    // S is comparable: it is a struct type whose fields are comparable.
    // (Field A is comparable because the type-set of 'any' includes at least one comparable type.)
    type S struct { A any }
    
    func (s *S) M() { s.A = func() {} }
    
    func main() {
        F(S{})  // This panics at run-time.
        // (The call to M() causes the resulting value to become incomparable, and then it is compared.)
    }

    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).)

  6. neild commented on Apr 29, 2022

    @neild
    Contributor

    The type set of the comparable interface includes all comparable types. (Thus, comparable is itself comparable.)

    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.

  7. bcmills commented on Apr 29, 2022

    @bcmills
    ContributorAuthor

    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.

    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. 😅

  8. bcmills commented on Apr 29, 2022

    @bcmills
    ContributorAuthor

    @neild, updated with more precise language based on the current spec.

  9. bcmills commented on Apr 29, 2022

    @bcmills
    ContributorAuthor

    (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.)

  10. neild commented on Apr 29, 2022

    @neild
    Contributor

    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. sweat_smile

    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.

    var a any
    var b any = int(0)
    
    // This stores the value stored in b (type int), not b (type any).
    a = b
    

    Therefore error (for example) is not in the type set of any, because you can't store a value of type error in a variable of type any.

    You can, however, assign a value of type error to a variable of type any, because error implements any.

  11. griesemer commented on Apr 29, 2022

    @griesemer
    Contributor

    So in this example given above

    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
    }

    the two calls of Eq are Eq[T](a, b) and Eq[any](a, b) (after type inference). The type set of T is the type set of its constraint which is any. So the instantiation of Eq[T] is not permitted (while Eq[any] is) because T is a type parameter and not an ordinary interface (even though the type sets seem identical)? Do I have that right?

  12. bcmills commented on Apr 29, 2022

    @bcmills
    ContributorAuthor

    The type set of T is the type set of its constraint which is any.

    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 T is indeed equal to the type set of any.

    the instantiation of Eq[T] is not permitted (while Eq[any] is) because T is a type parameter and not an ordinary interface (even though the type sets seem identical)? Do I have that right?

    More or less, yes: a type parameter is not necessarily an interface type.
    (Eq[T] is not permitted because T is not guaranteed to implement comparable — T and 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.)

  13. griesemer commented on Apr 29, 2022

    @griesemer
    Contributor

    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.

  14. 20 remaining items

  15. jimmyfrasche commented on May 5, 2022

    @jimmyfrasche
    Member

    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}.

  16. bcmills commented on May 6, 2022

    @bcmills
    ContributorAuthor

    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 comparable for the “a literal type” in that sentence, and you have a perfect analogy (https://go.dev/play/p/a0NmWliZNyG):

    	var f F  // type F struct{}
    	var a A  // type A = struct{}
    	var b B  // type B struct{}
    
    	a = f // F is assignable to A
    	b = a // A is assignable to B
    	b = f // F is not assignable to B

    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.

  17. bcmills commented on May 6, 2022

    @bcmills
    ContributorAuthor

    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:

    1. Assignment
      a) x is assignable to i if typeset(x) ⊆ typeset(i)

    but this proposal would have an additional caveat:

    1. Assignment
      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 comparable

    In addition, I think both proposals need an explicit rule for type-assertions. For this one:

    1. Assertability
      a) x.(I) succeeds if T ⊆ typeset(I) (where T is the dynamic type of x) and either the value of x is comparable or I does not embed comparable
  18. moved this to Incoming in Proposalson Aug 10, 2022
  19. changed the title [-]proposal: the `comparable` interface represents the comparable subset of run-time values[/-] [+]proposal: spec: allow values of type comparable[/+] on Aug 10, 2022
  20. ianlancetaylor commented on Nov 4, 2022

    @ianlancetaylor
    Contributor

    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.

  21. moved this from Incoming to Hold in Proposalson Nov 4, 2022
  22. qiulaidongfeng commented on Jan 18, 2024

    @qiulaidongfeng
    Member

    If we adopt #56548 we will not be adopting this proposal.

    Look like this issue can be close.

  23. griesemer commented on Jan 18, 2024

    @griesemer
    Contributor

    #56548 was implemented, so we don't need this anymore.

  24. locked and limited conversation to collaborators on Jan 17, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions