Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

cmd/go2go: cannot infer generic interface types #41176

Open
rogpeppe opened this issue Sep 2, 2020 · 11 comments
Open

cmd/go2go: cannot infer generic interface types #41176

rogpeppe opened this issue Sep 2, 2020 · 11 comments

Comments

@rogpeppe
Copy link
Contributor

@rogpeppe rogpeppe commented Sep 2, 2020

commit 8ea0120

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()
}
@dmitshur
Copy link
Contributor

@dmitshur dmitshur commented Sep 2, 2020

@dmitshur dmitshur added this to the Unreleased milestone Sep 2, 2020
@ianlancetaylor
Copy link
Contributor

@ianlancetaylor ianlancetaylor commented Sep 2, 2020

I agree that the current inference algorithm doesn't permit this inference. I'm not at all sure that it should. It requires that we go beyond looking at the type of the argument, to looking at the method set of the argument. And as far as I can see it would only be useful to infer the type parameter of a parameterized interface type. There is a pretty long step from argument method set to type argument for interface type. Our goal for type inference is not all possible type inference, it's straightforward and easily understood type inference. At least at first glance, I don't think this meets that goal.

@griesemer
Copy link
Contributor

@griesemer griesemer commented Sep 2, 2020

Marked as FeatureRequest as this is not a bug in the prototype.

@dmitshur
Copy link
Contributor

@dmitshur dmitshur commented Sep 3, 2020

@griesemer An issue should have one of NeedsInvestigation, NeedsDecision, or NeedsFix labels after being triaged (per https://golang.org/wiki/HandlingIssues#issue-states). I'll re-add NeedsInvestigation, but please feel to change it if another Needs label is more appropriate.

@griesemer
Copy link
Contributor

@griesemer griesemer commented Sep 3, 2020

@dmitshur ACK. Thanks for the reminder.

@rogpeppe
Copy link
Contributor Author

@rogpeppe rogpeppe commented Sep 3, 2020

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.

@ianlancetaylor
Copy link
Contributor

@ianlancetaylor ianlancetaylor commented Sep 5, 2020

One of the common requests for the design draft is support for methods that have their own generic type parameters. We don't currently know how to implement that (https://go.googlesource.com/proposal/+/refs/heads/master/design/go2draft-type-parameters.md#no-parameterized-methods). But if ever do figure out how to implement it, there is going to be some sort of relationship to parameterized interface types. I at least don't fully understand how this should all work together, so I'm wary of adding type inference into the mix.

@rogpeppe
Copy link
Contributor Author

@rogpeppe rogpeppe commented Sep 7, 2020

But if ever do figure out how to implement it, there is going to be some sort of relationship to parameterized interface types.

I'm sure there will be, but won't there also be some sort of relationship to concrete types too, and also to non-parameterized interface types? AFAICS adding methods with their own type parameters will largely be orthogonal to type inference in general, because the method type parameters are free and thus unrelated to the outer level type parameters.

As I see it, type inference on interfaces is not that far off from type inference on constants, which has good treatment in the proposal.

@rogpeppe
Copy link
Contributor Author

@rogpeppe rogpeppe commented Jan 27, 2021

Here's a little motivational example: https://go2goplay.golang.org/p/4wLqRDSTNZS

Function types and single-method interfaces are generally quite similar, so it's surprising,
I believe, that type inference works when passing a function value, but not when passing
an interface implementation.

It's very common in Go to pass a concrete implementation and have it implicitly
converted to an interface type. I'm pretty sure that type inference should work here.

package main

import (
	"fmt"
)

func main() {
	i := intGetter(99)
	// This works fine.
	fmt.Println(getf(i.get))
	// This also works fine.
	fmt.Println(geti(getter[int](i)))
	// ... but not when the argument is a concrete
	// type that implements the required interface.
	// An explicit [int] type parameter is needed.
	fmt.Println(geti(i))
}

type getter[T any] interface {
	get() T
}

type getterFunc[T any] func() T

func getf[T any](f getterFunc[T]) T {
	return f()
}

func geti[T any](f getter[T]) T {
	return f.get()
}

type intGetter int

func (i intGetter) get() int {
	return int(i)
}
@ianlancetaylor
Copy link
Contributor

@ianlancetaylor ianlancetaylor commented Jan 27, 2021

I think that what we are talking about here is extending the type inference rules so that if the function parameter's type T is a parameterized interface type I, then given type argument A we infer the arguments to I based on the methods of A. We would presumably do this by walking through the methods of I, finding the identically named methods of A, and trying to unify the types of the two methods. I agree that the results are likely to be unsurprising if A is a fully concrete type. 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.

Maximizing potential type inference is definitely not a goal. The primary guideline is that type inference must never be surprising. A secondary guideline is that it must come up often enough to be useful. 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?

@rogpeppe
Copy link
Contributor Author

@rogpeppe rogpeppe commented Jan 27, 2021

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 the io.Pipe to pipe batches of floating point numbers, for example).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Linked pull requests

Successfully merging a pull request may close this issue.

None yet
4 participants