Updating tools to support type parameters.
This guide is maintained by Rob Findley (rfindley@google.com
).
status: this document is currently a rough-draft. See golang/go#50447 for more details.
- Who should read this guide
- Introduction
- Summary of new language features and their APIs
- Examples
- Generic types: type parameters
- Constraint Interfaces
- Instantiation
- Generic types continued: method sets and predicates
- Updating tools while building at older Go versions
- Further help
Who should read this guide
Read this guide if you are a tool author seeking to update your tools to
support generics Go code. Generics introduce significant new complexity to the
Go type system, because types can now be parameterized. While the
fundamentals of the go/types
APIs remain the same, some previously valid
assumptions no longer hold. For example:
- Type declarations need not correspond 1:1 with the types they define.
- Interfaces are no longer determined entirely by their method set.
- The set of concrete types implementing
types.Type
has grown to include
types.TypeParam
and types.Union
.
Introduction
With Go 1.18, Go now supports generic programming via type parameters. This
document is a guide for tool authors that want to update their tools to support
the new language constructs.
This guide assumes knowledge of the language changes to support generics. See
the following references for more information:
It also assumes knowledge of go/ast
and go/types
. If you're just getting
started,
x/example/gotypes is
a great introduction (and was the inspiration for this guide).
Summary of new language features and their APIs
The introduction of of generic features appears as a large change to the
language, but a high level introduces only a few new concepts. We can break
down our discussion into the following three broad categories: generic types,
constraint interfaces, and instantiation. In each category below, the relevant
new APIs are listed (some constructors and getters/setters may be elided where
they are trivial):
Generic types. Types and functions may be generic, meaning their
declaration may have a non-empty type parameter list, as in
type List[T any] ...
or func f[T1, T2 any]() { ... }
. Type parameter lists
define placeholder types (type parameters), scoped to the declaration, which
may be substituted by any type satisfying their corresponding constraint
interface to instantiate a new type or function.
Generic types may have methods, which declare receiver type parameters
via
their receiver type expression: func (r T[P1, ..., PN]) method(...) (...) {...}
.
New APIs:
- The field
ast.TypeSpec.TypeParams
holds the type parameter list syntax for
type declarations. - The field
ast.FuncType.TypeParams
holds the type parameter list syntax for
function declarations. - The type
types.TypeParam
is a types.Type
representing a type parameter.
On this type, the Constraint
and SetConstraint
methods allow
getting/setting the constraint, the Index
method returns the numeric index
of the type parameter in the type parameter list that declares it, and the
Obj
method returns the object in the scope a for the type parameter (a
types.TypeName
). Generic type declarations have a new *types.Scope
for
type parameter declarations. - The type
types.TypeParamList
holds a list of type parameters. - The method
types.Named.TypeParams
returns the type parameters for a type
declaration. - The method
types.Named.SetTypeParams
sets type parameters on a defined
type. - The function
types.NewSignatureType
creates a new (possibly generic)
signature type. - The method
types.Signature.RecvTypeParams
returns the receiver type
parameters for a method. - The method
types.Signature.TypeParams
returns the type parameters for
a function.
Constraint Interfaces: type parameter constraints are interfaces, expressed
by an interface type expression. Interfaces that are only used in constraint
position are permitted new embedded elements composed of tilde expressions
(~T
) and unions (A | B | ~C
). The new builtin interface type comparable
is implemented by types for which ==
and !=
are valid (note that interfaces
must be statically comparable in this case, i.e., each type in the interface's
type set must be comparable). As a special case, the interface
keyword may be
omitted from constraint expressions if it may be implied (in which case we say
the interface is implicit).
New APIs:
- The constant
token.TILDE
is used to represent tilde expressions as an
ast.UnaryExpr
. - Union expressions are represented as an
ast.BinaryExpr
using |
. This
means that ast.BinaryExpr
may now be both a type and value expression. - The method
types.Interface.IsImplicit
reports whether the interface
keyword was elided from this interface. - The method
types.Interface.MarkImplicit
marks an interface as being
implicit. - The method
types.Interface.IsComparable
reports whether every type in an
interface's type set is comparable. - The method
types.Interface.IsMethodSet
reports whether an interface is
defined entirely by its methods (has no specific types). - The type
types.Union
is a type that represents an embedded union
expression in an interface. May only appear as an embedded element in
interfaces. - The type
types.Term
represents a (possibly tilde) term of a union.
Instantiation: generic types and functions may be instantiated to create
non-generic types and functions by providing type arguments (var x T[int]
).
Function type arguments may be inferred via function arguments, or via
type parameter constraints.
New APIs:
- The type
ast.IndexListExpr
holds index expressions with multiple indices,
as in instantiation expressions with multiple type arguments or in receivers
declaring multiple type parameters. - The function
types.Instantiate
instantiates a generic type with type arguments. - The type
types.Context
is an opaque instantiation context that may be
shared to reduce duplicate instances. - The field
types.Config.Context
holds a shared Context
to use for
instantiation while type-checking. - The type
types.TypeList
holds a list of types. - The type
types.ArgumentError
holds an error associated with a specific
type argument index. Used to represent instantiation errors. - The field
types.Info.Instances
maps instantiated identifiers to information
about the resulting type instance. - The type
types.Instance
holds information about a type or function
instance. - The method
types.Named.TypeArgs
reports the type arguments used to
instantiate a named type.
Examples
The following examples demonstrate the new APIs, and discuss their properties.
All examples are runnable, contained in subdirectories of the directory holding
this README.
Generic types: type parameters
We say that a type is generic if it has type parameters but no type
arguments. This section explains how we can inspect generic types with the new
go/types
APIs.
Type parameter lists
Suppose we want to understand the generic library below, which defines a generic
Pair
, a constraint interface Constraint
, and a generic function MakePair
.
package main
type Constraint interface {
Value() any
}
type Pair[L, R any] struct {
left L
right R
}
func MakePair[L, R Constraint](l L, r R) Pair[L, R] {
return Pair[L, R]{l, r}
}
We can use the new TypeParams
fields in ast.TypeSpec
and ast.FuncType
to
access the type parameter list. From there, we can access type parameter types
in at least three ways:
- by looking up type parameter definitions in
types.Info
- by calling
TypeParams()
on types.Named
or types.Signature
- by looking up type parameter objects in the declaration scope. Note that
there now may be a scope associated with an
ast.TypeSpec
node.
func PrintTypeParams(fset *token.FileSet, file *ast.File) error {
conf := types.Config{Importer: importer.Default()}
info := &types.Info{
Scopes: make(map[ast.Node]*types.Scope),
Defs: make(map[*ast.Ident]types.Object),
}
_, err := conf.Check("hello", fset, []*ast.File{file}, info)
if err != nil {
return err
}
// For convenience, we can use ast.Inspect to find the nodes we want to
// investigate.
ast.Inspect(file, func(n ast.Node) bool {
var name *ast.Ident // the name of the generic object, or nil
var tparamFields *ast.FieldList // the list of type parameter fields
var tparams *types.TypeParamList // the list of type parameter types
var scopeNode ast.Node // the node associated with the declaration scope
switch n := n.(type) {
case *ast.TypeSpec:
name = n.Name
tparamFields = n.TypeParams
tparams = info.Defs[name].Type().(*types.Named).TypeParams()
scopeNode = n
case *ast.FuncDecl:
name = n.Name
tparamFields = n.Type.TypeParams
tparams = info.Defs[name].Type().(*types.Signature).TypeParams()
scopeNode = n.Type
default:
// Not a type or function declaration.
return true
}
// Option 1: find type parameters by looking at their declaring field list.
if tparamFields != nil {
fmt.Printf("%s has a type parameter field list with %d fields\n", name.Name, tparamFields.NumFields())
for _, field := range tparamFields.List {
for _, name := range field.Names {
tparam := info.Defs[name]
fmt.Printf(" field %s defines an object %q\n", name.Name, tparam)
}
}
} else {
fmt.Printf("%s does not have a type parameter list\n", name.Name)
}
// Option 2: find type parameters via the TypeParams() method on the
// generic type.
if tparams.Len() > 0 {
fmt.Printf("%s has %d type parameters:\n", name.Name, tparams.Len())
for i := 0; i < tparams.Len(); i++ {
tparam := tparams.At(i)
fmt.Printf(" %s has constraint %s\n", tparam, tparam.Constraint())
}
} else {
fmt.Printf("%s does not have type parameters\n", name.Name)
}
// Option 3: find type parameters by looking in the declaration scope.
scope, ok := info.Scopes[scopeNode]
if ok {
fmt.Printf("%s has a scope with %d objects:\n", name.Name, scope.Len())
for _, name := range scope.Names() {
fmt.Printf(" %s is a %T\n", name, scope.Lookup(name))
}
} else {
fmt.Printf("%s does not have a scope\n", name.Name)
}
return true
})
return nil
}
This program produces the following output. Note that not every type spec has
a scope.
> go run golang.org/x/tools/internal/typeparams/example/findtypeparams
Constraint does not have a type parameter list
Constraint does not have type parameters
Constraint does not have a scope
Pair has a type parameter field list with 2 fields
field L defines an object "type parameter L any"
field R defines an object "type parameter R any"
Pair has 2 type parameters:
L has constraint any
R has constraint any
Pair has a scope with 2 objects:
L is a *types.TypeName
R is a *types.TypeName
MakePair has a type parameter field list with 2 fields
field L defines an object "type parameter L hello.Constraint"
field R defines an object "type parameter R hello.Constraint"
MakePair has 2 type parameters:
L has constraint hello.Constraint
R has constraint hello.Constraint
MakePair has a scope with 4 objects:
L is a *types.TypeName
R is a *types.TypeName
l is a *types.Var
r is a *types.Var
Constraint Interfaces
In order to allow operations on type parameters, Go 1.18 introduces the notion
of type sets, which is
abstractly the set of types that implement an interface. This section discusses
the new syntax for restrictions on interface type sets, and the APIs we can use
to understand them.
New interface elements
Consider the generic library below:
package p
type Numeric interface{
~int | ~float64 // etc...
}
func Square[N Numeric](n N) N {
return n*n
}
type Findable interface {
comparable
}
func Find[T Findable](s []T, v T) int {
for i, v2 := range s {
if v2 == v {
return i
}
}
return -1
}
In this library, we can see a few new features added in Go 1.18. The first is
the new syntax in the Numeric
type: unions of tilde-terms, specifying that
the numeric type may only be satisfied by types whose underlying type is int
or float64
.
The go/ast
package parses this new syntax as a combination of unary and
binary expressions, which we can see using the following program:
func PrintNumericSyntax(fset *token.FileSet, file *ast.File) {
// node is the AST node corresponding to the declaration for "Numeric."
node := file.Scope.Lookup("Numeric").Decl.(*ast.TypeSpec)
// Find the embedded syntax node.
embedded := node.Type.(*ast.InterfaceType).Methods.List[0].Type
// Use go/ast's built-in Print function to inspect the parsed syntax.
ast.Print(fset, embedded)
}
Output:
0 *ast.BinaryExpr {
1 . X: *ast.UnaryExpr {
2 . . OpPos: p.go:6:2
3 . . Op: ~
4 . . X: *ast.Ident {
5 . . . NamePos: p.go:6:3
6 . . . Name: "int"
7 . . }
8 . }
9 . OpPos: p.go:6:7
10 . Op: |
11 . Y: *ast.UnaryExpr {
12 . . OpPos: p.go:6:9
13 . . Op: ~
14 . . X: *ast.Ident {
15 . . . NamePos: p.go:6:10
16 . . . Name: "float64"
17 . . }
18 . }
19 }
Once type-checked, these embedded expressions are represented using the new
types.Union
type, which flattens the expression into a list of *types.Term
.
We can also investigate two new methods of interface:
types.Interface.IsComparable
, which reports whether the type set of an
interface is comparable, and types.Interface.IsMethodSet
, which reports
whether an interface is expressable using methods alone.
func PrintInterfaceTypes(fset *token.FileSet, file *ast.File) error {
conf := types.Config{}
pkg, err := conf.Check("hello", fset, []*ast.File{file}, nil)
if err != nil {
return err
}
PrintIface(pkg, "Numeric")
PrintIface(pkg, "Findable")
return nil
}
func PrintIface(pkg *types.Package, name string) {
obj := pkg.Scope().Lookup(name)
fmt.Println(obj)
iface := obj.Type().Underlying().(*types.Interface)
for i := 0; i < iface.NumEmbeddeds(); i++ {
fmt.Printf("\tembeded: %s\n", iface.EmbeddedType(i))
}
for i := 0; i < iface.NumMethods(); i++ {
fmt.Printf("\tembeded: %s\n", iface.EmbeddedType(i))
}
fmt.Printf("\tIsComparable(): %t\n", iface.IsComparable())
fmt.Printf("\tIsMethodSet(): %t\n", iface.IsMethodSet())
}
Output:
type hello.Numeric interface{~int|~float64}
embeded: ~int|~float64
IsComparable(): true
IsMethodSet(): false
type hello.Findable interface{comparable}
embeded: comparable
IsComparable(): true
IsMethodSet(): false
The Findable
type demonstrates another new feature of Go 1.18: the comparable
built-in. Comparable is a special interface type, not expressable using
ordinary Go syntax, whose type-set consists of all comparable types.
Implicit interfaces
For interfaces that do not have methods, we can inline them in constraints and
elide the interface
keyword. In the example above, we could have done this
for the Square
function:
package p
func Square[N ~int|~float64](n N) N {
return n*n
}
In such cases, the types.Interface.IsImplicit
method reports whether the
interface type was implicit. This does not affect the behavior of the
interface, but is captured for more accurate type strings:
func ShowImplicit(pkg *types.Package) {
Square := pkg.Scope().Lookup("Square").Type().(*types.Signature)
N := Square.TypeParams().At(0)
constraint := N.Constraint().(*types.Interface)
fmt.Println(constraint)
fmt.Println("IsImplicit:", constraint.IsImplicit())
}
Output:
~int|~float64
IsImplicit: true
The types.Interface.MarkImplicit
method is used to mark interfaces as
implicit by the importer.
Type sets
The examples above demonstrate the new APIs for accessing information about
the new interface elements, but how do we understand
type sets, the new
abstraction that these elements help define? Type sets may be arbitrarily
complex, as in the following example:
package complex
type A interface{ ~string|~[]byte }
type B interface{ int|string }
type C interface { ~string|~int }
type D interface{ A|B; C }
Here, the type set of D
simplifies to ~string|int
, but the current
go/types
APIs do not expose this information. This will likely be added to
go/types
in future versions of Go, but in the meantime we can use the
typeparams.NormalTerms
helper:
func PrintNormalTerms(pkg *types.Package) error {
D := pkg.Scope().Lookup("D").Type()
terms, err := typeparams.NormalTerms(D)
if err != nil {
return err
}
for i, term := range terms {
if i > 0 {
fmt.Print("|")
}
fmt.Print(term)
}
fmt.Println()
return nil
}
which outputs:
~string|int
See the documentation for typeparams.NormalTerms
for more information on how
this calculation proceeds.
Instantiation
We say that a type is instantiated if it is created from a generic type by
substituting type arguments for type parameters. Instantiation can occur via
explicitly provided type arguments, as in the expression T[A_1, ..., A_n]
, or
implicitly, through type inference.. This section describes how to find and
understand instantiated types.
Finding instantiated types
Certain applications may find it useful to locate all instantiated types in
a package. For this purpose, go/types
provides a new types.Info.Instances
field that maps instantiated identifiers to information about their instance.
For example, consider the following code:
package p
type Pair[L, R comparable] struct {
left L
right R
}
func (p Pair[L, _]) Left() L {
return p.left
}
func Equal[L, R comparable](x, y Pair[L, R]) bool {
return x.left == y.left && x.right == y.right
}
var X Pair[int, string]
var Y Pair[string, int]
var E = Equal[int, string]
We can find instances by type-checking with the types.Info.Instances
map
initialized:
func CheckInstances(fset *token.FileSet, file *ast.File) (*types.Package, error) {
conf := types.Config{}
info := &types.Info{
Instances: make(map[*ast.Ident]types.Instance),
}
pkg, err := conf.Check("p", fset, []*ast.File{file}, info)
for id, inst := range info.Instances {
posn := fset.Position(id.Pos())
fmt.Printf("%s: %s instantiated with %s: %s\n", posn, id.Name, FormatTypeList(inst.TypeArgs), inst.Type)
}
return pkg, err
}
Output:
hello.go:21:9: Equal instantiated with [int, string]: func(x p.Pair[int, string], y p.Pair[int, string]) bool
hello.go:10:9: Pair instantiated with [L, _]: p.Pair[L, _]
hello.go:14:34: Pair instantiated with [L, R]: p.Pair[L, R]
hello.go:18:7: Pair instantiated with [int, string]: p.Pair[int, string]
hello.go:19:7: Pair instantiated with [string, int]: p.Pair[string, int]
The types.Instance
type provides information about the (possibly inferred)
type arguments that were used to instantiate the generic type, and the
resulting type. Notably, it does not include the generic type that was
instantiated, because this type can be found using types.Info.Uses[id].Type()
(where id
is the identifier node being instantiated).
Note that the receiver type of method Left
also appears in the Instances
map. This may be counterintuitive -- more on this below.
Creating new instantiated types
go/types
also provides an API for creating type instances:
types.Instantiate
. This function accepts a generic type and type arguments,
and returns an instantiated type (or an error). The resulting instance may be
a newly constructed type, or a previously created instance with the same type
identity. To facilitate the reuse of frequently used instances,
types.Instantiate
accepts a types.Context
as its first argument, which
records instances.
If the final validate
argument to types.Instantiate
is set, the provided
type arguments will be verified against their corresponding type parameter
constraint; i.e., types.Instantiate
will check that each type arguments
implements the corresponding type parameter constraint. If a type arguments
does not implement the respective constraint, the resulting error will wrap
a new ArgumentError
type indicating which type argument index was bad.
func Instantiate(pkg *types.Package) error {
Pair := pkg.Scope().Lookup("Pair").Type()
X := pkg.Scope().Lookup("X").Type()
Y := pkg.Scope().Lookup("Y").Type()
// X and Y have different types, because their type arguments are different.
Compare("X", "Y", X, Y)
// Create a shared context for the subsequent instantiations.
ctxt := types.NewContext()
// Instantiating with [int, string] yields an instance that is identical (but
// not equal) to X.
Int, String := types.Typ[types.Int], types.Typ[types.String]
inst1, _ := types.Instantiate(ctxt, Pair, []types.Type{Int, String}, true)
Compare("X", "inst1", X, inst1)
// Instantiating again returns the same exact instance, because of the shared
// Context.
inst2, _ := types.Instantiate(ctxt, Pair, []types.Type{Int, String}, true)
Compare("inst1", "inst2", inst1, inst2)
// Instantiating with 'any' is an error, because any is not comparable.
Any := types.Universe.Lookup("any").Type()
_, err := types.Instantiate(ctxt, Pair, []types.Type{Int, Any}, true)
var argErr *types.ArgumentError
if errors.As(err, &argErr) {
fmt.Printf("Argument %d: %v\n", argErr.Index, argErr.Err)
}
return nil
}
func Compare(leftName, rightName string, left, right types.Type) {
fmt.Printf("Identical(%s, %s) : %t\n", leftName, rightName, types.Identical(left, right))
fmt.Printf("%s == %s : %t\n\n", leftName, rightName, left == right)
}
Output:
Identical(p.Pair[int, string], p.Pair[string, int]) : false
p.Pair[int, string] == p.Pair[string, int] : false
Identical(p.Pair[int, string], p.Pair[int, string]) : true
p.Pair[int, string] == p.Pair[int, string] : false
Identical(p.Pair[string, int], p.Pair[int, string]) : false
p.Pair[string, int] == p.Pair[int, string] : false
Identical(p.Pair[int, string], p.Pair[int, string]) : true
p.Pair[int, string] == p.Pair[int, string] : true
Argument 1: any does not implement comparable
Using a shared context while type checking
To share a common types.Context
argument with a type-checking pass, set the
new types.Config.Context
field.
Generic types continued: method sets and predicates
Generic types are fundamentally different from ordinary types, in that they may
not be used without instantiation. In some senses they are not really types:
the go spec defines types as "a set of
values, together with operations and methods", but uninstantiated generic types
do not define a set of values. Rather, they define a set of types. In that
sense, they are a "meta type", or a "type template" (disclaimer: I am using
these terms imprecisely).
However, for the purposes of go/types
it is convenient to treat generic types
as a types.Type
. This section explains how generic types behave in existing
go/types
APIs.
Method Sets
Methods on uninstantiated generic types are different from methods on an
ordinary type. Consider that for an ordinary type T
, the receiver base type
of each method in its method set is T
. However, this can't be the case for
a generic type: generic types cannot be used without instantation, and neither
can the type of the receiver variable. Instead, the receiver base type is an
instantiated type, instantiated with the method's receiver type parameters.
This has some surprising consequences, which we observed in the section on
instantiation above: for a generic type G
, each of its methods will define
a unique instantiation of G
, as each method has distinct receiver type
parameters.
To see this, consider the following example:
package p
type Pair[L, R any] struct {
left L
right R
}
func (p Pair[L, _]) Left() L {
return p.left
}
func (p Pair[_, R]) Right() R {
return p.right
}
var IntPair Pair[int, int]
Let's inspect the method sets of the types in this library:
func PrintMethods(pkg *types.Package) {
// Look up *Named types in the package scope.
lookup := func(name string) *types.Named {
return pkg.Scope().Lookup(name).Type().(*types.Named)
}
Pair := lookup("Pair")
IntPair := lookup("IntPair")
PrintMethodSet("Pair", Pair)
PrintMethodSet("Pair[int, int]", IntPair)
LeftObj, _, _ := types.LookupFieldOrMethod(Pair, false, pkg, "Left")
LeftRecvType := LeftObj.Type().(*types.Signature).Recv().Type()
PrintMethodSet("Pair[L, _]", LeftRecvType)
}
func PrintMethodSet(name string, typ types.Type) {
fmt.Println(name + ":")
methodSet := types.NewMethodSet(typ)
for i := 0; i < methodSet.Len(); i++ {
method := methodSet.At(i).Obj()
fmt.Println(method)
}
fmt.Println()
}
Output:
Pair:
func (p.Pair[L, _]).Left() L
func (p.Pair[_, R]).Right() R
Pair[int, int]:
func (p.Pair[int, int]).Left() int
func (p.Pair[int, int]).Right() int
Pair[L, _]:
func (p.Pair[L, _]).Left() L
func (p.Pair[L, _]).Right() _
In this example, we can see that all of Pair
, Pair[int, int]
, and
Pair[L, _]
have distinct method sets, though the method set of Pair
and
Pair[L, _]
intersect in the Left
method.
Only the objects in Pair
's method set are recorded in types.Info.Defs
. To
get back to this "canonical" method object, the typeparams
package provides
the OriginMethod
helper:
func CompareOrigins(pkg *types.Package) {
Pair := pkg.Scope().Lookup("Pair").Type().(*types.Named)
IntPair := pkg.Scope().Lookup("IntPair").Type().(*types.Named)
Left, _, _ := types.LookupFieldOrMethod(Pair, false, pkg, "Left")
LeftInt, _, _ := types.LookupFieldOrMethod(IntPair, false, pkg, "Left")
fmt.Println("Pair.Left == Pair[int, int].Left:", Left == LeftInt)
origin := typeparams.OriginMethod(LeftInt.(*types.Func))
fmt.Println("Pair.Left == OriginMethod(Pair[int, int].Left):", Left == origin)
}
Output:
Pair.Left == Pair[int, int].Left: false
Pair.Left == OriginMethod(Pair[int, int].Left): true
Predicates
Predicates on generic types are not defined by the spec. As a consequence,
using e.g. types.AssignableTo
with operands of generic types leads to an
undefined result.
The behavior of predicates on generic *types.Named
types may generally be
derived from the fact that type parameters bound to different names are
different types. This means that most predicates involving generic types will
return false
.
*types.Signature
types are treated differently. Two signatures are considered
identical if they are identical after substituting one's set of type parameters
for the other's, including having identical type parameter constraints. This is
analogous to the treatment of ordinary value parameters, whose names do not
affect type identity.
Consider the following code:
func OrdinaryPredicates(pkg *types.Package) {
var (
Pair = pkg.Scope().Lookup("Pair").Type()
LeftRighter = pkg.Scope().Lookup("LeftRighter").Type()
Mer = pkg.Scope().Lookup("Mer").Type()
F = pkg.Scope().Lookup("F").Type()
G = pkg.Scope().Lookup("G").Type()
H = pkg.Scope().Lookup("H").Type()
)
fmt.Println("AssignableTo(Pair, LeftRighter)", types.AssignableTo(Pair, LeftRighter))
fmt.Println("AssignableTo(Pair, Mer): ", types.AssignableTo(Pair, Mer))
fmt.Println("Identical(F, G)", types.Identical(F, G))
fmt.Println("Identical(F, H)", types.Identical(F, H))
}
Output:
AssignableTo(Pair, LeftRighter) false
AssignableTo(Pair, Mer): true
Identical(F, G) true
Identical(F, H) false
In this example, we see that despite their similarity the generic Pair
type
is not assignable to the generic LeftRighter
type. We also see the rules for
signature identity in practice.
This begs the question: how does one ask questions about the relationship
between generic types? In order to phrase such questions we need more
information: how does one relate the type parameters of Pair
to the type
parameters of LeftRighter
? Does it suffice for the predicate to hold for one
element of the type sets, or must it hold for all elements of the type sets?
We can use instantiation to answer some of these questions. In particular, by
instantiating both Pair
and LeftRighter
with the type parameters of Pair
,
we can determine if, for all type arguments [X, Y]
that are valid for Pair
,
[X, Y]
are also valid type arguments of LeftRighter
, and Pair[X, Y]
is
assignable to LeftRighter[X, Y]
. The typeparams.GenericAssignableTo
function implements exactly this predicate:
func GenericPredicates(pkg *types.Package) {
var (
Pair = pkg.Scope().Lookup("Pair").Type()
LeftRighter = pkg.Scope().Lookup("LeftRighter").Type()
)
fmt.Println("GenericAssignableTo(Pair, LeftRighter)", typeparams.GenericAssignableTo(nil, Pair, LeftRighter))
}
Output:
GenericAssignableTo(Pair, LeftRighter) true
Updating tools while building at older Go versions
In the examples above, we can see how a lot of the new APIs integrate with
existing usage of go/ast
or go/types
. However, most tools still need to
build at older Go versions, and handling the new language constructs in-line
will break builds at older Go versions.
For this purpose, the x/exp/typeparams
package provides functions and types
that proxy the new APIs (with stub implementations at older Go versions).
Further help
If you're working on updating a tool to support generics, and need help, please
feel free to reach out for help in any of the following ways: