Description
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:
func F[A ~int](A) {}
func G[B ~int](B) {}
func H[C int](C) {}
type T[D ~int] func(D)
In this example, the types of F
and G
are considered identical, because they have pairwise identical type parameter constraints and their signatures are identical after substituting type parameters. However, the signature of F
is NOT identical to the signature of H
(constraints don't match), and NOT identical to the underlying type of T
(the signature itself does not have a type parameter list -- it uses type parameters from the declaration for T
).
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:
type Interface[T any] interface {
Accept(T)
}
type Container[T any] struct {
Element T
}
func (c Container[T]) Accept(t T) { c.Element = t }
Previously, we would report that Container
is assignable to Interface
(AssignableTo(Container, Interface) == true
), because they would unify. However, this could be wrong, as in the following example:
type InterfaceInt[T ~int] interface {
Accept(T)
}
type ContainerString[T ~string] struct {
Element T
}
func (c ContainerString[T]) Accept(t T) { c.Element = t }
We would also have AssignableTo(ContainerString, InterfaceInt) == true
, even though there is no instantiation for which this holds.
Related:
- cmd/go: go vet reports error:
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. - go/types: method signatures not identical modulo receiver type parameter names #49912 -- an outstanding bug for comparing method signatures modulo type parameters. However, notably fixing this bug would not address the behavior with respect to the
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"
func _[A *C, B *C, C any]() {
var X func(A)
var Y func(B)
}
type I[P *Q, Q any] interface{
m(P)
}
The type of X
and Y
should not be identical. What about the type of X
and the signature of I.m
? (EDIT: X
and Y
do NOT have identical types).