How to define a type constraint for both primitives and defined types?

Viewed 89

I have a use case where I'd like to be able to compare two values and see which one is larger, but I'd also like to be able for composite types to participate by implementing an interface.

Something like

type Ordered interface {
  ~int | ~float32 | ~string // ...
}

type OrderedValue[T Ordered] interface {
   Value() T
}

Then I'd like my Compare function to be able accept either two ordered arguments and return the result, or two OrderedValues and call the Value() method to do compare. Something like:

func doCompare[T Ordered](a, b T) int {
  if a > b {
    return 1
  } else if a < b {
    return -1
  }
  return 0
}

func Compare[U Ordered, V OrderedValue[U], T U | V](a, b T) int {
  var i interface{} = a
  if _, ok := i.(OrderedValue[U]); ok {
    return doCompare(a.Value(), b.Value())
  }
  return doCompare(a, b)
}

For the Compare function I get error message

cannot embed a type parameter

And when calling doCompare in the interface case I get

a.Value undefined (type T has no field or method Value)

b.Value undefined (type T has no field or method Value)

I have seen one question here that implies this may not be possible, but this one How to iterate over a union of slices passed in a generic function? (T has no core type) gave me hope that there may be a clever way of doing this that I'm just not seeing.

I was hoping to avoid having to define Box implementations for built-in types to support this. Any guidance would be appreciated.

Here is a go playground that has more complete example

1 Answers

You can't (sanely).

The constraint you want can be summarized as "ordered primitive or method that gives an ordered primitive". This can't be expressed with Go 1.18 because

  • you can't have other type parameters in unions
  • you can't have interfaces with methods in unions

It's possible to cook out something that compiles, but it would be very weird, and it wouldn't take advantage of type inference.

I believe the cleanest solution at this time is just two different functions for primitives and defined types:

// import "golang.org/x/exp/constraints"

type Orderable[T constraints.Ordered] interface {
   Value() T
}

func ComparePrimitive[T constraints.Ordered](a, b T) int {
    return doCompare(a, b)
}

// type inference works only if a and b concrete types are instantiations of Orderable
// otherwise you have to instantiate T explicitly
func CompareValuer[T constraints.Ordered](a, b Orderable[T]) int {
    return doCompare(a.Value(), b.Value())
}

If future Go releases relax the limitations on non-basic interfaces and constraints, such as allowing non-basic interfaces such as constraints.Ordered in type switches, or allowing method-only interfaces in unions, then this might have nicer solutions.

As of Go 1.18, you should stick to using type parameters for things that behave exactly the same.

Related