README
¶
Updating tools to support type parameters.
This guide is maintained by Rob Findley (rfindley@google.com).
status: this document is currently a work-in-progress. See golang/go#50447 for more details.
- Introduction
- Summary of new language features and their APIs
- Examples
- Updating tools while building at older Go versions
- Further help
Introduction
With Go 1.18, Go now supports generic programming via type parameters. This document is intended to serve as a guide for tool authors that want to update their tools to support the new language constructs introduced for generic Go.
This guide assumes some knowledge of the language changes to support generics. See the following references for more information:
- The original proposal for type parameters.
- The addendum for type sets.
- The latest language specfication (still in-progress as of 2021-01-11).
- The proposals for new APIs in go/token and go/ast, and in go/types.
It also assumes existing 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
While generic Go programming is a large change to the language, at a high level it introduces only a few new concepts. Specifically, we can break down our discussion into the following three broad categories. In each category, 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 has 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.TypeParamsholds the type parameter list syntax for type declarations. - The field
ast.FuncType.TypeParamsholds the type parameter list syntax for function declarations. - The type
types.TypeParamis atypes.Typerepresenting a type parameter. On this type, theConstraintandSetConstraintmethods allow getting/setting the constraint, theIndexmethod returns the index of the type parameter in the type parameter list that declares it, and theObjmethod returns the object declared in the declaration scope for the type parameter (atypes.TypeName). - The type
types.TypeParamListholds a list of type parameters. - The method
types.Named.TypeParamsreturns the type parameters for a type declaration. - The method
types.Named.SetTypeParamssets type parameters on a defined type. - The function
types.NewSignatureTypecreates a new (possibly generic) signature type. - The method
types.Signature.RecvTypeParamsreturns the receiver type parameters for a method. - The method
types.Signature.TypeParamsreturns the type parameters for a function.
Constraint Interfaces: type parameter constraints are interfaces, expressed
via 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. 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.TILDEis used to represent tilde expressions as anast.UnaryExpr. - Union expressions are represented as an
ast.BinaryExprusing|. This means thatast.BinaryExprmay now be both a type and value expression. - The method
types.Interface.IsImplicitreports whether theinterfacekeyword was elided from this interface. - The method
types.Interface.MarkImplicitmarks an interface as being implicit. - The method
types.Interface.IsComparablereports whether every type in an interface's type set is comparable. - The method
types.Interface.IsMethodSetreports whether an interface is defined entirely by its methods (has no specific types). - The type
types.Unionis a type that represents an embedded union expression in an interface. May only appear as an embedded element in interfaces. - The type
types.Termrepresents 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.IndexListExprholds index expressions with multiple indices, as occurs in instantiation expressions with multiple type arguments, or in receivers with multiple type parameters. - The function
types.Instantiateinstantiates a generic type with type arguments. - The type
types.Contextis an opaque instantiation context that may be shared to reduce duplicate instances. - The field
types.Config.Contextholds a sharedContextto use for instantiation while type-checking. - The type
types.TypeListholds a list of types. - The type
types.ArgumentErrorholds an error associated with a specific argument index. Used to represent instantiation errors. - The field
types.Info.Instancesmaps instantiated identifiers to information about the resulting type instance. - The type
types.Instanceholds information about a type or function instance. - The method
types.Named.TypeArgsreports the type arguments used to instantiate a named type.
Examples
The following examples demonstrate the new APIs above, and discuss their properties. All examples are runnable, contained in subdirectories of the directory holding this README.
Generic types
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() interface{}
}
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 syntax of 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()ontypes.Namedortypes.Signature - by looking up type parameter objects in the declaration scope. Note that
there now may be a scope associated with an
ast.TypeSpecnode.
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 tparamSyntax *ast.FieldList // the list of type parameter fields
var tparamTypes *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
tparamSyntax = n.TypeParams
tparamTypes = info.Defs[name].Type().(*types.Named).TypeParams()
name = n.Name
scopeNode = n
case *ast.FuncDecl:
name = n.Name
tparamSyntax = n.Type.TypeParams
tparamTypes = info.Defs[name].Type().(*types.Signature).TypeParams()
scopeNode = n.Type
}
if name == nil {
return true // not a generic object
}
// Option 1: find type parameters by looking at their declaring field list.
if tparamSyntax != nil {
fmt.Printf("%s has a type parameter field list with %d fields\n", name.Name, tparamSyntax.NumFields())
for _, field := range tparamSyntax.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.
fmt.Printf("%s has %d type parameters:\n", name.Name, tparamTypes.Len())
for i := 0; i < tparamTypes.Len(); i++ {
tparam := tparamTypes.At(i)
fmt.Printf(" %s has constraint %s\n", tparam, tparam.Constraint())
}
// 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 has 0 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
Methods on generic types
TODO
Constraint Interfaces
New interface elements
TODO
Implicit interfaces
TODO
Type sets
TODO
Instantiation
Finding instantiated types
TODO
Creating new instantiated types
TODO
Using a shared context
TODO
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).
NOTE: does not yet exist -- see
golang/go#50447 for more information.
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:
- Via the golang-tools mailing list.
- Directly to me via email (
rfindley@google.com). - For bugs, you can file an issue.