This is a short response to the recently announced Go 2 generics draft proposals
Update: This proposal is incomplete. It cannot replace two common use cases. The first is ensuring that several formal parameters are of the same type:
contract comparable(t T) {
t > t
}
func max(type T comparable)(a, b T) T
Here a
, and b
must be the same parameterised type — my suggestion would only assert that they had at least the same contract.
Secondly the it would not be possible to parameterise the type of return values:
contract viaStrings(t To, f From) {
var x string = f.String()
t.Set(string(""))
}
func SetViaStrings(type To, From viaStrings)(s []From) []To
Thanks to Ian Dawes and Sam Whited for their insight.
Bummer.
My lasting reaction to the Generics proposal is the proliferation of parenthesis in function declarations.
Although several of the Go team suggested that generics would probably be used sparingly, and the additional syntax would only be burden for the writer of the generic code, not the reader, I am sceptical that this long requested feature will be sufficiently niche as to be unnoticed by most Go developers.
It is true that type parameters can be inferred from their arguments, the declaration of generic functions and methods require a clumsy (type
parameter declaration in place of the more common <T>
syntaxes found in C++ and Java.
The reason for (type
, it was explained to me, is Go is designed to be parsed without a symbol table. This rules out both <T>
and [T]
syntaxes as the parser needs ahead of time what kind of declaration a T
is to avoid interpreting the angle or square braces as comparison or indexing operators respectively.
Contract as a superset of interfaces
The astute Roger Peppe quickly identified that contracts represent a superset of interfaces
Any behaviour you can express with an interface, you can do so and more, with a contract.
The remainder of this post are my suggestions for an alternative generic function declaration syntax that avoids add additional parenthesis by leveraging Roger’s observation.
Contract as a kind of type
The earlier Type Functions proposal showed that a type
declaration can support a parameter. If this is correct, then the proposed contract
declaration could be rewritten from
contract stringer(x T) {
var s string = x.String()
}
to
type stringer(x T) contract {
var s string = x.String()
}
This supports Roger’s observation that a contract
is a superset of an interface
. type stringer(x T) contract { ... }
introduces a new contract
type in the same way type stringer interface { ... }
introduces a new interface
type.
If you buy my argument that a contract
is a kind of type
is debatable, but if you’re prepared to take it on faith then the remainder of the syntax introduced in the generics proposal could be further simplified.
If contracts are types, use them as types
If a contract
is an identifier then we can use a contract
anywhere that a built-in type or interface is used. For example
func Stringify(type T stringer)(s []T) (ret []string) {
for _, v := range s {
ret = append(ret, v.String())
} return ret
}
Could be expressed as
func Stringify(s []stringer) (ret []string) {
for _, v := range s {
ret = append(ret, v.String())
} return ret
}
That is, in place of explicitly binding T
to the contract stringer
only for T
to be referenced seven characters later, we bind the formal parameter s
to a slice of stringer
s directly. The similarity with the way this would previously be done with a stringer
interface emphasises Roger’s observation.
Unifying unknown type parameters
The first example in the design proposal introduces an unknown type parameter.
func Print(type T)(s []T) {
for _, v := range s {
fmt.Println(v)
}
}
The operations on unknown types are limited, they are in some senses values that can only be read. Again drawing on Roger’s observation above, the syntax could potentially be expressed as:
func Print(s []contract{}) {
for _, v := range s {
fmt.Println(v)
}
}
Or maybe even
type T contract {} func Print(s []T) {
for _, v := range s {
fmt.Println(v)
}
}
In essence the literal contract{}
syntax defines an anonymous unknown type analogous to interface{}
‘s anonymous interface type.
Conclusion
The great irony is, after years of my bloviation that “adding generics to Go has nothing to do with the syntax”
2, it turns out that, actually, yes, the syntax is crucial.
-
type stringer interface { String() string } func Stringify(s []stringer) (ret []string) { for _, v := range s { ret = append(ret, v.String()) } return ret }
- If you want to read my non answer to the question of “should generics be added to Go?”, you can do so here.