Tim Reddy
Tim Reddy

Reputation: 4430

Cannot use variable of type *T as type in argument

I'm learning Go 1.18 generics and I'm trying to understand why I'm having trouble here. Long story short, I'm trying to Unmarshal a protobuf and I want the parameter type in blah to "just work". I've simplified the problem as best I could, and this particular code is reproducing the same error message I'm seeing:

./prog.go:31:5: cannot use t (variable of type *T) as type stringer in argument to do:
    *T does not implement stringer (type *T is pointer to type parameter, not type parameter)
package main

import "fmt"

type stringer interface {
    a() string
}

type foo struct{}

func (f *foo) a() string {
    return "foo"
}

type bar struct{}

func (b *bar) a() string {
    return "bar"
}

type FooBar interface {
    foo | bar
}

func do(s stringer) {
    fmt.Println(s.a())
}

func blah[T FooBar]() {
    t := &T{}
    do(t)
}

func main() {
    blah[foo]()
}

I realize that I can completely simplify this example by not using generics (i.e., pass the instance to blah(s stringer) {do(s)}. However, I do want to understand why the error is happening.

What do I need to change with this code so that I can create an instance of T and pass that pointer to a function expecting a particular method signature?

Upvotes: 6

Views: 14679

Answers (2)

imakiri
imakiri

Reputation: 1

In addition to the answer above there are two more ways to solve this problem. In both cases the FooBar type has to be:

type FooBar interface {
    *foo | *bar
    stringer
}

The first one:

Create this generic New func. It does the same as built-in new func, but can be easily passed as a function argument.

func New[T any]() *T {
    return new(T)
}

Modify the blah func to accept and use the _new func. This function is responsible for creating a FooBar instance.

func blah[T FooBar](_new func() T) {
    t := _new()
    do(t)
}

Modify the main func:

func main() {
    blah(New[foo])
}

Basically we're injecting the built-in new function into a generic function.

Full solution on a go playground

The second one:

We can use reflection to instantiate a variable of a pointer type hidden behind an interface:

func gnew[T any]() T {
    var t T
    var _T = reflect.TypeOf(t)
    if _T.Kind() == reflect.Pointer {
        return reflect.New(_T.Elem()).Interface().(T)
    }
    return t
}

The blah and the main func will be:

func blah[T FooBar]() {
    var t = gnew[T]()
    do(t)
}

func main() {
    blah[*foo]()
}

Full solution on a go playground

Upvotes: 0

blackgreen
blackgreen

Reputation: 44647

In your code there's no relationship between the constraints FooBar and stringer. Furthermore the methods are implemented on the pointer receivers.

A quick and dirty fix for your contrived program is simply to assert that *T is indeed a stringer:

func blah[T FooBar]() {
    t := new(T)
    do(any(t).(stringer))
}

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

But this forgoes type safety, and could panic at run time. To preserve compile time type safety, another solution that somewhat preserves your programs' semantics would be this:

type FooBar[T foo | bar] interface {
    *T
    stringer
}

func blah[T foo | bar, U FooBar[T]]() {
    var t T
    do(U(&t))
}

So what's going on here?

First, the relationship between a type parameter and its constraint is not identity: T is not FooBar. You cannot use T like it was FooBar, therefore *T is definitely not equivalent to *foo or *bar.

So when you call do(t), you're attempting to pass a type *T into something that expects a stringer, but T, pointer or not, just does not inherently have the a() string method in its type set.

Step 1: add the method a() string into the FooBar interface (by embedding stringer):

type FooBar interface {
    foo | bar
    stringer
}

But that's not enough yet, because now none of your types actually implement it. Both declare the method on the pointer receiver.

Step 2: change the types in the union to be pointers:

type FooBar interface {
    *foo | *bar
    stringer
}

This constraint now works, but you have another problem. You can't declare composite literals when the constraint doesn't have a core type. So t := T{} is also invalid. We change it to:

func blah[T FooBar]() {
    var t T // already pointer type
    do(t)
}

Now this compiles, but t is actually the zero value of a pointer type, so it's nil. Your program doesn't crash because the methods just return some string literal.

If you need to also initialize the memory referenced by the pointers, inside blah you need to know about the base types.

Step 3: So you add T foo | bar as one type param, and change the signature to:

func blah[T foo | bar, U FooBar]() {
    var t T
    do(U(&t))
}

Done? Not yet. The conversion U(&t) is still invalid because the type set of both U and T don't match. You need to now parametrize FooBar in T.

Step 4: basically you extract FooBar's union into a type param, so that at compile time its type set will include only one of the two types:

type FooBar[T foo | bar] interface {
    *T
    stringer
}

The constraint now can be instantiated with T foo | bar, preserve type safety, pointer semantics and initialize T to non-nil.

func (f *foo) a() string {
    fmt.Println("foo nil:", f == nil)
    return "foo"
}

func main() {
    blah[foo]()
}

Prints:

foo nil: false
foo

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


If you can instantiate blah with pointer types, or even better pass arguments to it, you can remove all the intermediate trickery:

type FooBar interface {
    *foo | *bar
    stringer
}

func blah[T FooBar](t T) {
    do(t)
}

func main() {
    blah(&foo{})
}

Upvotes: 8

Related Questions