Personally, I think it's best to not include in a union a lot of types that are unrelated to each other, as they wouldn't share many common operations, and you end up writing type-specific code. So then what's the point of using generics...?
Anyway, possible strategies depend on what is included in the type set of SupportedType constraint, and what you want to do with those:
Only exact types and no methods
Use a type switch on T and run whatever operation makes sense for the concrete type. This works best when the method implementation works with only one value of type T, as you can work directly with the variable in the switch guard (v := any(vec[a]).(type)). It stops being pretty when you have more T values beside the one in the switch guard, as you have to convert & assert all of them individually:
func (vec Vec[T]) Less(a, b int) bool {
switch v := any(vec[a]).(type) {
case int64:
return v < any(vec[b]).(int64)
case time.Time:
return v.Before(any(vec[b]).(time.Time))
// more cases...
}
return false
}
With methods
Parametrize the interface that contains the methods and constrain its T to the supported types. Then constrain Vector's type parameter to both.
The advantage of this one is to make sure that Vector can't be instantiated with types on which you forgot to implement Less(T) bool and get rid of the type assertion, which otherwise, could panic at runtime.
type Lesser[T SupportedType] interface {
Less(T) bool
}
type Vec[T interface { SupportedType; Lesser[T] }] []T
func (vec Vec[T]) Less(a, b int) bool {
return vec[a].Less(vec[b])
}
With methods and predeclared types
Impossible. Consider the following:
type SupportedTypes interface {
// exact predeclared types
int | string
}
type Lesser[T SupportedTypes] interface {
Less(T) bool
}
The constraint Lesser has an empty type set, because neither int nor string can have methods. So here you fall back to the "exact types and no methods" case.
With approximate types (~T)
Changing the constraints above into approximate types:
type SupportedTypes interface {
// approximate types
~int | ~string
}
type Lesser[T SupportedTypes] interface {
Less(T) bool
}
The type switch is not an option, since case ~int: isn't legal. And the presence of a method on the constraint prevents you from instantiating with the predeclared types:
Vector[MyInt8]{} // ok when MyInt8 implements Lesser
Vector[int8] // doesn't compile, int8 can't implement Lesser
So the options I see then are:
- force client code to use defined types, which in many cases might be just fine
- reduce the scope of the constraint to types that support the same operations
- reflection (benchmark to see if the performance penalty is too much for you), but reflection can't actually find the underlying type, so you're left with some hacks using
reflect.Kind or CanConvert.
This might improve and possibly trump other options when/if this proposal comes through.