-
Notifications
You must be signed in to change notification settings - Fork 17.8k
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
go/types, types2: document predicates on generic types #50887
Comments
Why do you say that? In my experience, it's always possible to write type-checker test cases that exercise identity rules. E.g., adding
I think It seems kind of a niche use case though. If a type parameter can only be instantiated by a single type, there's not much reason to have it be a parameter in the first place. |
Oops, that was a broken example, sorry (initially I was using local type declarations, and didn't fix the example properly). A and B are definitely not identical there. UPDATED: the example illustrates that the type of |
CC @schroederc for awareness that beta2 will break kythe edges between generic types, and that we're aware of the problem. |
I see. The question is about how to handle type identity for parameterized types that haven't yet been instantiated? Is there a use case for defining identity on such types? If not, I'm inclined to say type identity is undefined on parameterized, non-instantiated types, and types.Identical should panic if it finds such a type. |
Since generic types/functions can only be used in instantiated form (at which point they are not generic anymore) I believe @findleyr is correct that this shouldn't affect type checking of source code. With respect to the API behavior, we need to determine the behavior for If we have bound type parameters only, i.e., type parameters declared with the generic function/type, I believe we are in agreement that generic functions/types are identical if they can be made identical by renaming type parameters. With respect to "free" type parameters, it seems that there are two different kinds:
For 1), given a generic type Using the example from above: type Interface[T any] interface {
Accept(T)
}
type Container[T any] struct {
Element T
}
func (c Container[T]) Accept(t T) { c.Element = t } The respective method expression types are (pseudo code): func Interface.Accept[T any](Interface[T], T)
func Container.Accept[T any](Container[T], T) Ignoring the first parameter for signature identity leads to two identical function signatures. Doing the same with the second (invalid) example from above will lead to two different function signatures because the constraints don't match. Fo assignability, besides checking all methods, we also need to check assignment compatibility of the receiver type to the interface (because of type sets). Again, type parameters and constraints must be identical modulo renaming. For case 2) I think we can only compare types/functions that are declared within the same environment, i.e., enclosed within the same generic function. There can be only one such generic function enclosing parameterized types/functions because we don't allow function-local method, or type-local type declarations. So for the purposes of comparing generic function-local types or functions, it seems that type parameters by the enclosing function simply have to match exactly. In other words, local types/functions are always different from any other type or function that is not defined in the same scope. |
@griesemer and I had a long brainstorming session about this today, and finally arrived at the conclusion that there is no obvious way to define most predicates for uninstantiated generic types that Do The Right Thing. For one, the two
and above we discuss making these
but we don't want these
Why do we care about constraints in the last, but not the first? This is incoherent, and more generally we concluded that any decision about the meaning of predicates on generic types runs into inconsistent and/or confusing behavior. The problem is that there is more than one way to extend predicates to generic types: do all instantiations satisfy the predicate? Do any? Do we ignore constraints? Each of these could be useful interpretations in different situations. It's also a problem that some predicates (such as In the future we will probably want to expose APIs for substitution and unification that can efficiently answer any such question, but in the meantime we realized that the correct instantiations can answer many of them now, without additional APIs. For example https://go.dev/cl/383094 extends So the good news is that we don't think any significant changes or new APIs are necessary for 1.18*, and this exercise (as well as related discussions around method identity) have given us a lot of insight into the APIs we should be thinking about for 1.19. *well actually, we will probably want to remove the special handling that was added to |
Actually, I think what we have now is correct. The original reason for this decision still holds: for the purposes of type identity, it is consistent to treat signature type parameters the same way we treat ordinary parameters, ignoring their names. It is also helpful: with the current behavior (ignoring names), if two generic signatures are identical they will behave the same during instantiation: If we just ignore type parameters entirely, we lose some of this useful behavior. The Furthermore, I no longer think that it's logically inconsistent not to ignore receiver type parameter names in signatures. As we've seen, methods are not really "generic" at all: they are all methods on an instantiated type -- instantiated with their receiver type parameters. This last point is rather subtle, and warrants exposition. |
Per #50887 (comment), I don't think there's anything more to do here other than try to document everything we've learned. To preserve history, I'll change the title of this issue to track updating our documentation. I'm also going to remove the release-blocker label. |
Change https://go.dev/cl/390039 mentions this issue: |
Change https://go.dev/cl/390575 mentions this issue: |
…ned on generic types Fixes #50887 Change-Id: I451d66b067badcfb7cf2e2756ea2b062366ac9d4 Reviewed-on: https://go-review.googlesource.com/c/go/+/390039 Trust: Robert Findley <rfindley@google.com> Run-TryBot: Robert Findley <rfindley@google.com> Reviewed-by: Robert Griesemer <gri@golang.org> TryBot-Result: Gopher Robot <gobot@golang.org> (cherry picked from commit 20dd9a4) Reviewed-on: https://go-review.googlesource.com/c/go/+/390575 Trust: Dmitri Shuralyov <dmitshur@golang.org> Run-TryBot: Dmitri Shuralyov <dmitshur@golang.org> Reviewed-by: Robert Findley <rfindley@google.com>
Right now, go/types has limited functionality for computing predicates "modulo type parameters": signatures are considered identical modulo renaming of type parameters in their type parameter list:
In this example, the types of
F
andG
are considered identical, because they have pairwise identical type parameter constraints and their signatures are identical after substituting type parameters. However, the signature ofF
is NOT identical to the signature ofH
(constraints don't match), and NOT identical to the underlying type ofT
(the signature itself does not have a type parameter list -- it uses type parameters from the declaration forT
).CL 379540 was intended to clean up some logic that existed to work around an earlier limitation that we didn't have instantiated methods. However, it inadvertently removed a "feature" that APIs like
AssignableTo
would somewhat work with uninstantiated types:We ran into this example in kythe:
Previously, we would report that
Container
is assignable toInterface
(AssignableTo(Container, Interface) == true
), because they would unify. However, this could be wrong, as in the following example:We would also have
AssignableTo(ContainerString, InterfaceInt) == true
, even though there is no instantiation for which this holds.Related:
impossible type assertion: no type can implement both... for generic interface
#50658 (comment). We don't expose any API for unification, and it would useful in several places.Interface
type above: the methods onInterface
are parameterized by the type declaration parameters, not receiver type parameters.Alternatively, callers could "instantiate" the types with identical arguments, but then the burden is pushed onto the caller to find type arguments that satisfy both type parameter lists, which is arbitrarily complicated.(EDIT: this wouldn't even really work: type parameters could be in arbitrary order, and so for the caller to even know which type parameters should correspond, they'd have to do the unification themselves).I don't believe this affects type-checking, but matters for the API and for tools. We need to reconsider what is most useful here.
My initial thoughts are as follows: rather than what we currently do for function signatures, generalize to a limited form of unification in
Identical
, where type parameters are joined and then substituted in constraints.This runs into a problem of "free type parameters"
The type of
X
andY
should not be identical. What about the type ofX
and the signature ofI.m
? (EDIT:X
andY
do NOT have identical types).CC @griesemer @mdempsky
The text was updated successfully, but these errors were encountered: