go: cmd/compile (types2, go/types): cannot infer generic interface types

commit 8ea0120a21b75837e38de2d04d9bc121757999e8

The type inference algorithm does not work when the actual parameter is a concrete type and the formal parameter is a generic interface type. It looks like the type inference description in the proposal doesn’t cover interfaces, but I suspect it should.

https://go2goplay.golang.org/p/C53vOfwA9vq

package main

type S struct {}

func (S) M() byte {
	return 0
}

type I[T any] interface {
	M() T
}

func F[T any](x I[T]) {
	x.M()
}

func main() {
	F(S{})
}

This fails with the error:

prog.go2:18:4: type S of (S literal) does not match I[T] (cannot infer T)

FWIW type interfence doesn’t work when the interface argument is a type parameter either, but I can’t work out if that’s covered by the proposal or not: https://go2goplay.golang.org/p/pAouk3xkmOX

func F[X I[T], T any](x X) {
	x.M()
}

About this issue

  • Original URL
  • State: closed
  • Created 4 years ago
  • Reactions: 27
  • Comments: 38 (23 by maintainers)

Commits related to this issue

Most upvoted comments

FWIW I ran across this when experimenting with making the io package generic. When dealing with generic interfaces I suspect it will be very common to pass a concrete implementation to a function that accepts the generic interface as an argument, just as in current Go it’s common to pass a concrete type to an interface parameter.

This issue will tend to arise when using any generic iterator interface AFAICS, so I think it would be worth addressing. It certainly seemed like it should work when I was adapting the code - I was surprised that it didn’t.

Change https://go.dev/cl/497657 mentions this issue: go/types, types2: enable interface inference

I think this limitation will encourage people to just stick extra identity methods onto their types to get the behavior they want.

Here’s an example of what I mean:

package main

import "fmt"

type A struct{}

func (a A) Open() string             { return "open" }
func (a A) AsOpener() Opener[string] { return a }

type B struct{}

func (b B) Open() bool             { return true }
func (b B) AsOpener() Opener[bool] { return b }

type Opener[X any] interface {
	Open() X
}

func Open[X any](o Opener[X]) X {
	x := o.Open()
	fmt.Printf("Opened Value: %v of type %T\n", x, x)
	return x
}

func main() {
	a, b := A{}, B{}

	Open(a.AsOpener())
	Open(b.AsOpener())
}

https://gotipplay.golang.org/p/fIS34g_-N6N

Practically speaking you wind up with the situation that in order to utilize A with Open, you can either type Open[string](a) or Open(a.AsOpener()). You’re incentivized to stick an identity method on your implementations of the interface and just use those methods instead. Now you use Open(a.AsOpener()) everywhere, and if you want to alter the definition of A to change the type to which it opens, you don’t have to update your call sites as you would if you had written Open[string](a) instead. Change the return type of Open() on A without changing AsOpener()? Compile time error. There you go. Now you can keep it all neatly wrapped up in one place, the compiler gives you all the guarantees it was already giving you, and you don’t have to state the type parameter of the generic interface at every callsite. With the current inferred limitations, why wouldn’t you do this every time you implement a generic interface?

And if you want to enforce that everyone in your codebase defines every Opener[X] to have this AsOpener method for consistency, you just define the Open interface to require it:

type Opener[X any] interface {
	Open() X
	AsOpener() Opener[X]
}

One other motivational example that I haven’t explicitly mentioned above: It’s common in Go to return a concrete type that implements an interface and adds extra methods to that interface (e.g. strings.Reader, bytes.Buffer, etc). If we want to do the same thing with type-parameterised interface types, there’s more friction because the type parameter must be passed explicitly.

This also needs a concrete proposal outlining what’s in scope.

If I’m identifying the right issue, I think I’ve been hitting against the lack of inference for generic interfaces recently.

In hope that it’s a useful example, we’ve been modelling modal forms in our app as the following:

type Modal[Props, State any] interface {
  DefaultState() *State
  BuildProps(context.Context, *State) (Props, error)
  Render(Props, *State) *slack.ModalViewRequest
}

With a lot omitted, but the basic principle being that you can implement a modal as a struct that operates on Props (built-at-runtime properties) and State (provided from the current state of the form) types.

One function that is very hard to write is an initial Render.

If we want to render the modal for the first time from the rest of the app, we’d like to build a generic Render function that can make this easy and type-safe.

Ideally:

func Render[Props, State any](modal Modal[Props, State], props Props) (*slack.ModalViewRequest, error) {
	return modal.Render(props, modal.DefaultState()).Build(modal)
}

So we can call it like so:

// We provide an IncidentPostCreate type, which implements DefaultState returning an
// IncidentPostCreateState pointer, which means we should be able to infer State.
Render(IncidentPostCreate{IncidentID: inc.ID}, IncidentPostCreateProps{
	Organisation: identity.Organisation,
	Incident:     inc,
})

But because Go doesn’t look at the first modal parameter type and understand that DefaultState() returns *State, and infer the State type from being provided with a modal, we’re forced to explicitly type the call:

// Required to provide Props and State
Render[IncidentPostCreateProps, IncidentPostCreateState](IncidentPostCreate{IncidentID: inc.ID}, IncidentPostCreateProps{
	Organisation: identity.Organisation,
	Incident:     inc,
})

It’s not the end of the world, but it is pretty ugly. We can do something convoluted so we only need provide a State type parameter, rather than both:

func RenderView[State, Props any, ModalType Modal[Props, State]](modal ModalType, props Props) (*slack.ModalViewRequest, error) {
	return modal.Render(props, modal.DefaultState()).Build(modal)
}

// This works, as we can infer Props from the function argument, and explicitly typing
// the parameters of the Modal interface means we can identify they relate.
//
// But we still need to provide State, even though ModalType must implement a DefaultState
// that could help us infer it.
Render[IncidentPostCreateState](IncidentPostCreate{IncidentID: inc.ID}, IncidentPostCreateProps{
	Organisation: identity.Organisation,
	Incident:     inc,
})

Of course, the compiler will shout if we ever provided types that didn’t match along these boundaries, so it felt intuitive that it could also make inferences.

This given with an understanding that full inference is a non-goal, but figured it would be worth an additional case study.

@arvidfm That is a different problem. This issue is about passing a non-generic type to a generic type and inferring the type argument to use for the generic type. Your example is about passing an instantiated type to a generic type, so no type inference is required to deduce the type argument; it’s right there in the instantiated type. Please open a separate issue. Thanks.

I wanted to offer that I’ve hit this in practice refactoring existing interface-heavy code to be generalized. In particular, this code has 4 very similar interfaces except that they each handle some different concrete type. This is a great use-case for generics!

I have a function like:

func handler.NewHandler[T any](r reducer.Reducers[T], vec handler.Vector[T], f filter.Filters[T]) handler.Handler[T]

handler.Vector[T] is an interface:

type Vector[T any] interface {
	Length() int
	Get(int) (*T, bool)
}

if I call the function:

handler.NewHandler(itn.Reducers(), vec, itn.Filters())

According to gopls, the first and last arguments infer properly, but vec does not, even though the argument is correct.

If I modify the call to:

handler.NewHandler[ConcreteType](itn.Reducers(), vec, itn.Filters())

The code compiles as expected.

It took me a long time staring at the error to understand what had occurred, as it felt very unexpected that the inference of this function was only partially supported.

It depends on how often people want to use interface types with type parameters. I don’t know often that will happen.

As one data point, in the generic code I’ve written, interface types with type parameters seem to come up about as often as interface types without type parameters in regular code. It’s often seems preferable to use an interface value rather than an interface constraint on a type parameter because it’s more flexible (such an interface can go inside a struct, for example, and be added later while preserving backward compatibility, something an additional type parameter cannot) and more ergonomic (type parameters tend to pollute all the code they touch, so having several type parameters is a pain because they have to passed through to every data structure and function that operates on them).

In fact, I wouldn’t be surprised if interface types with type parameters become more popular than constraints on type parameters because they’re more general. For example, a generic interface can define behaviour over arbitrary types that aren’t necessarily owned by the package implementing the interface.

For example, rather than defining graph type by mutually recursive constraint types, it’s arguably less awkward to define a graph like this:

type Graph[Node, Edge any] interface {
	Edges(n Node) []Edge
	Nodes(e Edge) (from, to Node)
}

func ShortestPath[Node, Edge any](graph Graph[Node, Edge]), from, to Node) []Edge

This allows the user to define a graph over any existing type that has a similar graph-like arrangement and also allows the interface methods to use arbitrary contextual information (stored in the Graph value) to influence behaviour.

Replying to @neild

Type inference never pays attention to assignabillity.

That said, there is something interesting about that example. We can infer T from the first argument: T must be int32. And at that point we have inferred all the type parameters. So this is different from the earlier examples. Here the problem is that function type inference always tries to unify all the function argument types with the type parameters. I think that is important for reasonable handling of untyped constants. But it’s not necessarily important for handling of typed arguments. For typed arguments we could skip cases where the types do not unify. In this example that would tell us that T is int32, and then postpone everything else to the actual function call type checking. In this case that would succeed through the implicit conversion to an interface type.

I don’t know whether that is a good idea or not. It depends on how often people want to use interface types with type parameters. I don’t know often that will happen. In particular, I would write this code like this, which works today.

package main

type Setter[T any] interface {
	Set(T)
}

type Int32Setter int32

func (v *Int32Setter) Set(x int32) { *v = (Int32Setter)(x) }

func SetAToB[T any, Set Setter[T]](x T, target Set) { target.Set(x) }
func SetBToA[T any, Set Setter[T]](target Set, x T) { target.Set(x) }

func main() {
	target := new(Int32Setter)
	value := int32(0)

	SetAToB[int32](value, target)         // ok
	SetBToA[int32](target, value)         // ok
	SetAToB(value, Setter[int32](target)) // ok
	SetBToA(Setter[int32](target), value) // ok
	SetAToB(value, target)                // ok
	SetBToA(target, value)                // ok
}

@griesemer Here’s another example. Here we’re passing an interface type that actually embeds the expected interface. I think it should be able to infer the type in this case at least.

package main

func main() {
	var x I2[int]
	F(x)
}

type I[T any] interface {
	Get() T
}

type I2[T any] interface {
	I[T]
	Foo()
}

func F[T any](i I[T]) {
}

I get the error:

./prog.go:5:4: type I2[int] of x does not match I[T] (cannot infer T)

I am much less certain about the case where A is a parameterized type, in particular where we are compiling a parameterized function for which A’s methods are defined by a constraint

The rules would obviously need to be thought about, and codified but my intuition is that the method set of any type is known (even if generically typed) and once we’ve established the set of common methods, unifying should be very similar to unifying func types in the existing proposal.

I have no idea whether people will write many parameterized interface types (other than for use as constraints). I don’t quite see the point of a parameterized interface type. Of course it must work, but will they be used frequently enough that it’s worth writing type inference rules for them?

I have only one data point: my own experience; but I’ve played around with a couple of not-entirely-trivial pieces of generic Go code (I ported a concurrent ordered map implementation and I made the stdlib io library generic).

In both cases, generic interfaces were a key part of the design, and I’m pretty sure that they will continue to be as useful in general as I found them there.

This is a Good Thing IMHO. It’s showing that Go’s existing generic feature, the interface value concept, is entirely orthogonal to, and composable with, the proposed new type parameter feature.

One nice property of generically typed interface types is that values inside them can “hide” their own type parameters, which is something that a struct type cannot, because any generic member of the struct must have its type parameters declared on the containing type. Thus generic interface types are an important part of the backward-compatibility story: they are a way of adding potentially generic functionality without changing existing type parameters (any change to type parameters being a backwardly incompatible change)

And that’s quite apart from the more direct applications such as for iterators (the io package being an example of a batched iterator package - it could be made generic without much difficulty. Why shouldn’t we be able to use io.Pipe to pipe batches of floating point numbers, for example?).

@dmitshur ACK. Thanks for the reminder.