Skip to content

proposal: spec: allow values of type comparable #52624

Closed as not planned
Closed as not planned
@bcmills

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions