go/types: make type bound instantiation optional if there's only one type parameter

This simplifies the use of parameterized interfaces as type bounds.
For example, given:

	type Bound(type T) interface { ... }

we can use this interface as a type bound with or without instantiation
in a type parameter list:

	func _(type T Bound(T))	...  // with explicit instantiation
	func _(type T Bound) ...     // instantiation left away

In the latter example, Bound is syntactic sugar for Bound(T).

In general, in a type parameter list, instantiation of an interface
type bound is optional iff the the type bound applies to exactly one
type parameter and the type bound expects exactly one type argument.

Change-Id: I0e820a0e214427cce6397069c1d5268494096812
Reviewed-on: https://team-review.git.corp.google.com/c/golang/go2-dev/+/728411
Reviewed-by: Robert Griesemer <gri@google.com>
This commit is contained in:
Robert Griesemer 2020-04-24 17:21:55 -07:00
parent 7c99cab23e
commit 4efa3ddb87
6 changed files with 100 additions and 5 deletions

View File

@ -155,3 +155,8 @@ DESIGN/IMPLEMENTATION
method signature, and 3) a type parameter followed by a type list. This is what the type
checker currently supports and the printer can print. (The parser still accepts a list of
method signatures or types, freely mixed.)
- 4/24/2020: Permit omission of explicit type bound instantiation if the type bound is an
interface that applies to exactly one type parameter and the type bound expects exactly
one type argument. This makes parameterized interface type bounds easier to use in a common
case.

View File

@ -672,7 +672,40 @@ func (check *Checker) collectTypeParams(list *ast.FieldList) (tparams []*TypeNam
}
// otherwise, bound must be an interface
if bound := expand(check.typ(f.Type)); IsInterface(bound) {
// TODO(gri) We should try to delay the IsInterface check
// as it may expand a possibly incomplete type.
if bound := check.anyType(f.Type); IsInterface(bound) {
// If we have exactly one type parameter and the type bound expects exactly
// one type argument, permit leaving away the type argument for the type
// bound. This allows us to write (type T B(T)) as (type T B) instead.
if isGeneric(bound) {
base := bound.(*Named) // only a *Named type can be generic
if len(f.Names) != 1 || len(base.tparams) != 1 {
// TODO(gri) make this error message better
check.errorf(f.Type.Pos(), "cannot use generic type %s without instantiation (more than one type parameter)", bound)
bound = Typ[Invalid]
goto next
}
// We have and expect exactly one type parameter.
// "Manually" instantiate the bound with the parameter.
// TODO(gri) this code (in more general form) is also in
// checker.typInternal for the *ast.CallExpr case. Factor?
typ := new(instance)
typ.check = check
typ.pos = f.Type.Pos()
typ.base = base
typ.targs = []Type{tparams[index].typ}
typ.poslist = []token.Pos{f.Names[0].Pos()}
// make sure we check instantiation works at least once
// and that the resulting type is valid
check.atEnd(func() {
t := typ.expand()
check.validType(t, nil)
})
// update bound and recorded type
bound = typ
check.recordTypeAndValue(f.Type, typexpr, typ, nil)
}
for i, _ := range f.Names {
setBoundAt(index+i, bound)
}

View File

@ -100,7 +100,8 @@ func _() {
xint = xbool // ERROR assignment
}
// Generic types cannot be used without instantiation.
// Generic types cannot be used without instantiation
// (except in special cases in type parameter lists).
var _ T // ERROR cannot use generic type T
// In type context, parameterized (generic) types cannot use instantiation syntax.
@ -199,3 +200,36 @@ func _(type T interface{ m(); type int })() {
y.m()
_ = y < 0
}
// As a special case, an explicit type parameter may be omitted
// from a type parameter bound if there is exactly one type
// parameter to which the type bound applies, and the type bound
// expects exactly one type argument.
type Adder(type T) interface {
Add(T) T
}
// We don't need to explicitly instantiate the Adder bound
// if we have exactly one type parameter.
func Sum(type T Adder)(list []T) T {
var sum T
for _, x := range list {
sum = sum.Add(x)
}
return sum
}
// Valid and invalid variations.
type B0 interface {}
type B1(type _) interface{}
type B2(type _, _) interface{}
func _(type T1 B0)()
func _(type T1 B1)()
func _(type T1 B2 /* ERROR cannot use generic type .* without instantiation */ )()
func _(type T1, T2 B0)()
func _(type T1, T2 B1 /* ERROR cannot use generic type .* without instantiation */ )()
func _(type T1, T2 B2 /* ERROR cannot use generic type .* without instantiation */ )()
func _(type T1 B0, T2 B1)() // here B1 applies to T2

View File

@ -272,7 +272,11 @@ func adderSum(type T Adder(T))(data []T) T {
return s
}
func _(type T Adder /* ERROR cannot use generic type Adder */)(data []T) T
// If we have exactly one type parameter and a type bound
// expecting exactly one type argument, we can leave the
// type argument away with the type bound as a form of
// syntactic sugar.
func _(type T Adder)(data []T) T
// --------------------------------------------------------------------------------------
// Type lists may contain type parameters... :-)

View File

@ -1,5 +1,15 @@
package p
type T(type P) P
type Adder(type T) interface {
Add(T) T
}
const _ T(int) = 0
// We don't need to explicitly instantiate the Adder bound
// if we have exactly one type parameter.
func Sum(type T Adder)(list []T) T {
var sum T
for _, x := range list {
sum = sum.Add(x)
}
return sum
}

View File

@ -133,6 +133,15 @@ func (check *Checker) typ(e ast.Expr) Type {
return check.definedType(e, nil)
}
// anyType type-checks the type expression e and returns its type, or Typ[Invalid].
// The type may be generic or instantiated.
func (check *Checker) anyType(e ast.Expr) Type {
typ := check.typInternal(e, nil)
assert(isTyped(typ))
check.recordTypeAndValue(e, typexpr, typ, nil)
return typ
}
// definedType is like typ but also accepts a type name def.
// If def != nil, e is the type specification for the defined type def, declared
// in a type declaration, and def.underlying will be set to the type of e before