-
Notifications
You must be signed in to change notification settings - Fork 17.9k
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/compile: invalidly inferred type for interface that doesn't use type parameter #60377
Comments
I'm not sure I follow. In my mind
That type T[P any] interface {
m() P
} now |
Consider this case: type T1[P any] interface {
m()
}
type T2[P any] interface {
m()
}
func g[P any](T1[P]) {}
func _() {
var x T2[int]
g(x) // here we infer P == int, but in fact any type of P would be ok
g[string](x) // here we set P == string
} Now we get an error for |
I see what you are saying. The interfaces-as-constraints are identical to each other and However, it seems like the parameterization of the interface-as-type should still be part of its identity. |
@jimmyfrasche that's why it's inference which would fail. But not instantiation. |
I expect it to behave like this, since the interface(s) aren't used as constraints and are just ordinary types: type T[P any] struct{}
func g[P any](T[P]) {}
func _() {
var x T[int]
g(x) // here we infer P == int
g[string](x) // here we fail to set P == string as P = int
} |
@jimmyfrasche But the rules for interface assignment are quite different from non-interface assignment. But here is a counter-example: type T[P any] interface{ m() }
func g[P any](func(T[P])) {}
func _() {
var f func(T[int])
g(f)
g[int](f)
g[string](f) // ERROR cannot use f (variable of type func(T[int])) as func(T[string]) value in argument to g[string]
} Because the interface is a component type, now it must match exactly. |
Ah, I was so fixated on the type inference I wasn't thinking about assignability. I still do not see why that implies that |
I believe type inference should only infer a type if it is the only one possible, if a valid type argument exists in the first place (ignoring the fact that in many cases This may mean that in some cases (like this one) we cannot infer a type argument. That's ok. |
I agree that type inference should not invent a type but I disagree that it should discard the provided type. I don't think this would matter much given how niche the edge case is, though. Would |
In other words, currently we have an exception for defined generic interface types: if a defined generic interface type If we consider this issue a bug, then a) we don't need an exception anymore, but b) in this example we wouldn't be able to infer I think this is a pretty esoteric case and doesn't really justify the exception. See also #8082 which applies when the interface types are components of other types. |
@jimmyfrasche the way I see it, inference is the reverse of instantiation. We know that a type parameter can only have one instantiation at a time. So the solution is unique. Each constructed type is unique and new: T[int] and T[string] are different but compatible in assignment to each other. That last part tripped me a little bit though, I admit 😅. This is the type identity/equality of interfaces that @griesemer linked and which impacts composite type definitions. But non generic go code still considers that two type definitions are different although possibly compatible. |
A type parameter can have only one instantiation at a time but that doesn't mean the solution to inference is unique: in the original example in this issue type inference infers |
Exactly, that's why inference fails. It's a case similar to f(x) = c where c is constant. Given c, we are still trying to find the one value of x that we passed to f. That's what was meant by unique, I don't disagree at all. |
I think the framing provided by @griesemer and @atdiar is useful: if the solution is not unique, inference should fail. Otherwise, we run the risk of inferring a different type in the future (and that's exactly why we need the special case currently). I'll note that there are other cases where there is one unique solution, and inference fails because of the current implementation:
|
@findleyr 's example is very convincing. Fix forthcoming. Clarification: It is better to not be able to infer a (possible) type argument when we could than to infer an incorrect type argument (which is the case in this example). The former is a shortcoming of the inference mechanism, while the latter is a bug. |
Change https://go.dev/cl/498315 mentions this issue: |
Here's an example where inference will continue to succeed, but infer a different type:
IMO this is an example of the current algorithm inferring the "wrong" type. The type of y is inferred to be float64, but should be int (and will be after this issue is fixed). |
@findleyr can inference work with untyped const values here? Perhaps it should fail here too because there are several solutions, 1 could be int or float32 or float64 etc...? https://go.dev/play/p/rHxWDwvTNek Edit: ha I think I see what you meant: https://go.dev/play/p/SHeQwzAeBRy That's indeed something that should be dealt with by the fix. |
@atdiar Your 2nd example will work with the fix. |
Change https://go.dev/cl/498895 mentions this issue: |
This change defines two unification modes used to control unification: - assign set when unifying types involved in an assignment - exact if set, types unify if they can be made identical Currently, unification is inexact: when a defined type is compared against a type literal, the underlying type of the defined type is considered. When channel types are compared, the channel direction is ignored. And when defined types are compared where one (or both) are interfaces, interface unification is used. By contrast, exact unification requires types to match exactly: if they can be unified, the types must be identical (with suitable type arguments). Exact unification is required when comparing component types. For instance, when unifying func(x P) with func(x Q), the two signatures unify only if P is identical to Q per Go's assignment rules. Until now we have ignored exact unification and made due with inexact unification everywhere, even for component types. In some cases this led to infinite recursions in the unifier, which we guarded against with a depth limit (and unification failure). Go's assignmemt rules allow inexact matching at the top-level but require exact matching for element types. This change passes 'assign' to the unifier when unifying parameter against argument types because those follow assignment rules. When comparing constraints, inexact unification is used as before. In 'assign' mode, when comparing element types, the unifyier is called recursively, this time with the 'exact' mode set, causing element types to be compared exactly. If unification succeeds for element types, they are identical (with suitable type arguments). This change fixes #60460. It also fixes a bug in the test for issue #60377. We also don't need to rely anymore on the recursion depth limit (a temporary fix) for #59740. Finally, because we use exact unification when comparing element types which are channels, errors caused by assignment failures (due to inexact inference which succeeded when it shouldn't have) now produce the correct inference error. Fixes #60460. For #60377. For #59740. Change-Id: Icb6a9b4dbd34294f99328a06d52135cb499cab85 Reviewed-on: https://go-review.googlesource.com/c/go/+/498895 Reviewed-by: Robert Findley <rfindley@google.com> Auto-Submit: Robert Griesemer <gri@google.com> Run-TryBot: Robert Griesemer <gri@google.com> Reviewed-by: Robert Griesemer <gri@google.com> TryBot-Result: Gopher Robot <gobot@golang.org>
This should be too late, and I am sorry if this was already discussed, but wouldn't it be possible to forbide unused type parameters by the Go compiler? In other words, couldn't Go compiler forbid the following code? type T[P any] interface {
m()
} I realized that unused type parameters tend to cause counterintuitive implicit conversions (https://go.dev/play/p/VLb031yF1z6, inspired by #61903). Also, unused parameters are unused then these should be useless. (If we really want to have a unused type parameter, we can add a member like What do you think? |
@hajimehoshi then someone will want this (probably with some good reason), asking: "Where are my phantom types?" :o) (phantom type is the usual name of this feature) Edit: now that I think of it, since you only want to disable it for interface types, I think it will depend on how the question of interface identity is solved. |
OK so my question is when phantom types are useful. EDIT: https://doc.rust-lang.org/rust-by-example/generics/phantom.html I see, so if they need to use phantom types, I was wondering if it would be possible to 'convince' the Go compiler by adding a member like |
@hajimehoshi in general to express units. type Distance[Unit any] float64
type Miles struct{}
type Meters struct{}
type DistanceMiles = Distance[Miles]
type DistanceMeters = Distance[Meters] I'd think. But your question is more interesting and I responded a bit too fast, since you are especially targeting the case of interface types. I'm curious as well to see what people think. |
Good point. Probably what I want to solve is counterintuitve assignment to an interface type. For non-interface types, such a counterintuitive assignment should not happen as far as I understand. So, forbidding an unused parameter for an interface type might be enough, beside consistency. |
In the following code:
we infer the type
int
forP
ofg
. But in fact any type would make this code work. Type inference should not succeed in this case.The text was updated successfully, but these errors were encountered: