Home > Back-end >  What is the best way to have polymorphic implementations on a generic in go (1.18)?
What is the best way to have polymorphic implementations on a generic in go (1.18)?

Time:03-20

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 or CanConvert.

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))
}

playground 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))
}

playground 2

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
  •  Tags:  
  • go
  • Related