'What is the best way to have polymorphic implementations on a generic in go (1.18)?
I want to create a Vector type that is generic over its internal data but may have differ in how the methods are implemented given the input type.
type SupportedType interface {
~int64 | ~uint64 | ~float64 | string | bool | time.Time
}
type Vec[T SupportedType] struct {
data []T
}
and I want to add a varying implementations on a function depending on the type. For example:
func (vec Vec[T]) Sort() {
...
}
In most of the generic types < will work just fine. However, if T -> time.Time I want to use the Before method and if T --> bool then I want all the false values to go before the true.
I have some ideas on how to accomplish this but what would be considered "idiomatic" in the new generics world? My application is performance sensitive.
Using a type union with types that all have the same function doesn't work (https://play.golang.com/p/QWE-XteWpjL).
Embedding a container inside type specific structs does work ( https://play.golang.com/p/j0AR48Mto-a ) but requires the use of an interface which means that the Less and Val in the example functions can't be inlined. It also might not work so nicely if there isn't a clean delineation between the subsets in the type union.
Solution 1:[1]
BTW there is already a library for sorting
https://pkg.go.dev/golang.org/x/exp/slices#Sort
1. You can create interface with generic, then type assert to that.
example:
type Lesser[T SupportedType] interface {
Less(T) bool
}
type Vec[T SupportedType] []T
func (vec Vec[T]) Less(a, b int) bool {
return any(vec[a]).(Lesser[T]).Less(vec[b])
}
func main() {
vs := Vec[String]([]String{"a", "b", "c", "d", "e"})
vb := Vec[Bool]([]Bool{false, true})
fmt.Println(vs.Less(3, 1))
fmt.Println(vb.Less(0, 1))
}
2. You can save the type on Vec.
example:
type Lesser[T SupportedType] interface {
Less(T) bool
}
type Vec[T SupportedType, L Lesser[T]] []T
func (vec Vec[T, L]) Less(a, b int) bool {
return any(vec[a]).(L).Less(vec[b])
}
func main() {
vs := Vec[String, String]([]String{"a", "b", "c", "d", "e"})
fmt.Println(vs.Less(3, 1))
}
3. Nesting type constraint
thanks @blackgreen
example :
type SupportedType interface {
Int8 | Time | Bool | String
}
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])
}
func main() {
vs := Vec[String]([]String{"a", "b", "c", "d", "e"})
fmt.Println(vs.Less(3, 1))
}
Benchmark:
benchmark 1 : 28093368 36.52 ns/op 16 B/op 1 allocs/op
benchmark 2 : 164784321 7.231 ns/op 0 B/op 0 allocs/op
benchmark 3 : 212480662 5.733 ns/op 0 B/op 0 allocs/op
Embedding a container inside type specific structs:
benchmark 4 : 211429621 5.720 ns/op 0 B/op 0 allocs/op
It's up to you which one is best for you. But IMO number 3 is best.
Solution 2:[2]
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.KindorCanConvert.
This might improve and possibly trump other options when/if this proposal comes through.
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | |
| Solution 2 |
