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.
CodePudding user response:
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
orCanConvert
.
This might improve and possibly trump other options when/if this proposal comes through.
CodePudding user response:
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))
}
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
Embedding a container inside type specific structs:
benchmark 3 : 211429621 5.720 ns/op 0 B/op 0 allocs/op