Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I instantiate a new pointer of type argument with generic Go?

Now that type parameters are available on golang/go:master, I decided to give it a try. It seems that I'm running into a limitation I could not find in the Type Parameters Proposal. (Or I must have missed it).

I want to write a function which returns a slice of values of a generic type with the constraint of an interface type. If the passed type is an implementation with a pointer receiver, how can we instantiate it?

type SetGetter[V any] interface {
    Set(V)
    Get() V
}

// SetGetterSlice turns a slice of type V into a slice of type T,
// with T.Set() called for each entry in values.
func SetGetterSlice[V any, T SetGetter[V]](values []V) []T {
    out := make([]T, len(values))

    for i, v := range values {
        out[i].Set(v) // panic if T has pointer receiver!
    }

    return out
}

When calling the above SetGetterSlice() function with the *Count type as T, this code will panic upon calling Set(v). (Go2go playground) To no surprise, as basically the code created a slice of nil pointers:


// Count implements SetGetter interface
type Count struct {
    x int
}

func (c *Count) Set(x int) { c.x = x }
func (c *Count) Get() int  { return c.x }

func main() {
    ints := []int{1, 2, 3, 4, 5}

    sgs := SetGetterSlice[int, *Count](ints)
    
    for _, s := range sgs {
        fmt.Println(s.Get())
    }
}

Variations of the same problem

This ideas won't work, and I can't seem to find any simple way to instantiate the pointed value.

  1. out[i] = new(T) will result in a compile failure, as it returns a *T where the type checker wants to see T.
  2. Calling *new(T), compiles but will result in the same runtime panic because new(T) returns **Count in this case, where the pointer to Count is still nil.
  3. Changing the return type to a slice of pointer to T will result in a compile failure:
func SetGetterSlice[V any, T SetGetter[V]](values []V) []*T {
    out := make([]*T, len(values))

    for i, v := range values {
        out[i] = new(T)
        out[i].Set(v) // panic if T has pointer receiver
    }

    return out
}

func main() {
    ints := []int{1, 2, 3, 4, 5}

    SetGetterSlice[int, Count](ints)
    // Count does not satisfy SetGetter[V]: wrong method signature
}

Workaround

The only solution I found until now, is to require a constructor function to be passed to the generic function. But this just feels wrong and a bit tedious. Why would this be required if func F(T interface{})() []T is perfectly valid syntax?

func SetGetterSlice[V any, T SetGetter[V]](values []V, constructor func() T) []T {
    out := make([]T, len(values))

    for i, v := range values {
        out[i] = constructor()
        out[i].Set(v)
    }

    return out
}

// ...
func main() {
    ints := []int{1, 2, 3, 4, 5}

    SetGetterSlice[int, *Count](ints, func() *Count { return new(Count) })
}

Summary

My questions, in order of priority:

  1. Am I overlooking something obvious?
  2. Is this a limitation of generics in Go and this is as good as it gets?
  3. Is this limitation known or should I raise an issue at the Go project?
like image 288
Tim Avatar asked Oct 14 '21 15:10

Tim


People also ask

How do you use generics in Go?

Using your text editor, create a file called main.go in the generics directory. You'll write your Go code in this file. Into main.go, at the top of the file, paste the following package declaration. A standalone program (as opposed to a library) is always in package main .

Does Go support generic programming?

Go already supported a form of generic programming via the use of empty interface types. For example, we can write a single function that works for different slice types by using an empty interface type with type assertions and type switches.

Why is Go adding generics?

You can simplify the above code to one function with generics, and it will work for all possible data types you pass. Generics would significantly reduce duplicated code in your codebase. You can also write other useful generic functions like map, reduce, filter, and so on for arrays and maps.

How to infer the type argument from other arguments in go?

The Go compiler can infer the type argument from other arguments. From the command line in the directory containing main.go, run the code. $ go run . Non-Generic Sums: 46 and 62.97 Generic Sums: 46 and 62.97 Generic Sums, type parameters inferred: 46 and 62.97 Generic Sums with Constraint: 46 and 62.97 Nicely done!

What is the use of type arguments in generic functions?

In calling the generic function you wrote, you specified type arguments that told the compiler what types to use in place of the function’s type parameters. As you’ll see in the next section, in many cases you can omit these type arguments because the compiler can infer them.

How do I use generics in go?

This tutorial introduces the basics of generics in Go. With generics, you can declare and use functions or types that are written to work with any of a set of types provided by calling code. In this tutorial, you’ll declare two simple non-generic functions, then capture the same logic in a single generic function. Create a folder for your code.

What is the difference between type constraints and type arguments?

While a type parameter’s constraint typically represents a set of types, at compile time the type parameter stands for a single type – the type provided as a type argument by the calling code. If the type argument’s type isn’t allowed by the type parameter’s constraint, the code won’t compile.


1 Answers

Basically you have to add one more type parameter to the constraint to make T convertible to its pointer type. In its most basic form, this technique looks like the following (with an anonymous constraint):

func Foo[T any, PT interface { *T; M() }]() {
    p := PT(new(T))
    p.M() // calling method on non-nil pointer
}

Playground: https://go.dev/play/p/L00tePwrDfx


Step by step solution

Your constraint SetGetter already declares a type param V, so we slightly modify the example above:

// V is your original type param
// T is the additional helper param
type SetGetter[V any, T any] interface {
    Set(V)
    Get() V
    *T
}

Then you define the SetGetterSlice function with the type parameter T any, whose purpose is just to instantiate the constraint SetGetter.

You will then be able to convert the expression &out[i] to the pointer type, and successfully call the method on the pointer receiver:

// T is the type with methods with pointer receiver
// PT is the SetGetter constraint with *T
func SetGetterSlice[V any, T any, PT SetGetter[V, T]](values []V) []T {
    out := make([]T, len(values))

    for i, v := range values {
        // out[i] has type T
        // &out[i] has type *T
        // PT constraint includes *T
        p := PT(&out[i]) // valid conversion!
        p.Set(v)         // calling with non-nil pointer receiver
    }

    return out
}

Full program:

package main

import (
    "fmt"
)

type SetGetter[V any, T any] interface {
    Set(V)
    Get() V
    *T
}

func SetGetterSlice[V any, T any, PT SetGetter[V, T]](values []V) []T {
    out := make([]T, len(values))

    for i, v := range values {
        p := PT(&out[i])
        p.Set(v)
    }

    return out
}

// Count implements SetGetter interface
type Count struct {
    x int
}

func (c *Count) Set(x int) { c.x = x }
func (c *Count) Get() int  { return c.x }

func main() {
    ints := []int{1, 2, 3, 4, 5}

    // instantiate with base type
    sgs := SetGetterSlice[int, Count](ints)

    for _, s := range sgs {
        fmt.Println(s.Get()) // prints 1,2,3,4,5 each in a newline
    }
}

This becomes more verbose because SetGetterSlice now requires three type parameters: the original V plus T (the type with pointer receivers) and PT (the new constraint). However when you call the function, you can omit the third one – with type inference, both type params V and T required to instantiate PT SetGetter[V,T] are already known:

SetGetterSlice[int, Count](ints)

Playground: https://go.dev/play/p/gcQZnw07Wp3

like image 91
blackgreen Avatar answered Nov 10 '22 22:11

blackgreen