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
- go/types, types2: consider shared methods when unifying against interfaces When unifying two types A and B where one or both of them are interfaces, consider the shared method signatures in unificati... — committed to golang/go by griesemer a year ago
- doc/go1.21: document type inference changes For #39661. For #41176. For #51593. For #52397. For #57192. For #58645. For #58650. For #58671. For #59338. For #59750. For #60353. Change-Id: Ib731c9f287... — committed to golang/go by griesemer a year ago
- doc/go1.21: document type inference changes For #39661. For #41176. For #51593. For #52397. For #57192. For #58645. For #58650. For #58671. For #59338. For #59750. For #60353. Change-Id: Ib731c9f287... — committed to golangFame/go by griesemer a year ago
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:
https://gotipplay.golang.org/p/fIS34g_-N6N
Practically speaking you wind up with the situation that in order to utilize
A
withOpen
, you can either typeOpen[string](a)
orOpen(a.AsOpener())
. You’re incentivized to stick an identity method on your implementations of the interface and just use those methods instead. Now you useOpen(a.AsOpener())
everywhere, and if you want to alter the definition ofA
to change the type to which it opens, you don’t have to update your call sites as you would if you had writtenOpen[string](a)
instead. Change the return type ofOpen()
onA
without changingAsOpener()
? 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 thisAsOpener
method for consistency, you just define theOpen
interface to require it: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:
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:
So we can call it like so:
But because Go doesn’t look at the first modal parameter type and understand that
DefaultState()
returns*State
, and infer theState
type from being provided with a modal, we’re forced to explicitly type the call: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:
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:
handler.Vector[T]
is an interface:if I call the function:
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:
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.
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:
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 beint32
. 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 thatT
isint32
, 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.
@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.
I get the error:
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 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.