Go generics have a new "type sets" way of doing type constraints
Any form of generics needs some way to constrain what types can be used with your generic functions (or generic types with methods), so that you can do useful things with them. The Go team's initial version of their generics proposal famously had a complicated method for this called "contracts", which looked like function bodies with some expressions in them. I (and other people) thought that this was rather too clever. After a lot of feedback, the Go team's revised second and third proposal took a more boring approach; the final design that was proposed and accepted used a version of Go interfaces for this purpose.
Using standard Go interfaces for type constraints has one limitation;
because they only define methods, a standard interface can't express
important constraints like 'the type must allow me to use <
on
its values' (or, in general, any operator). In order to deal with
this, the "type parameters" proposal that was accepted allowed an
addition to standard interfaces. Quoting from the issue's summary:
- Interface types used as type constraints can have a list of predeclared types; only type arguments that match one of those types satisfy the constraint.
- Generic functions may only use operations permitted by their type constraints.
Recently this changed to a new, more general, and more complicated approach that goes by the name of "type sets" (see also, and also). The proposal contains a summary of the new state of affairs, which I will quote (from the overview):
- Interface types used as type constraints can embed additional elements to restrict the set of type arguments that satisfy the constraint:
- an arbitrary type
T
restricts to that type- an approximation element
~T
restricts to all types whose underlying type isT
- a union element
T1 | T2 | ...
restricts to any of the listed elements- Generic functions may only use operations supported by all the types permitted by the constraint.
Unlike before, these embedded types don't have to be predeclared ones and may be composite types such as maps or structs, although somewhat complicated rules apply.
Type sets are more general and less hard coded than the initial
version, so I can see why the generics design has switched over to
them. But they're also more complicated (and more verbose), and I
worry that they contain a little trap that's ready to bite people
in the foot. The problem is that I think you'll almost always want
to use an approximation element, ~T
, but the arbitrary type element
T
is the default. If you just list off some types, your generics
are limited to exactly those types; you have to remember to add the
'~
' and then use the underlying type.
My personal view is that using type declarations for predeclared types
is a great Go feature, because it leads to greater type safety. I may
be using an int
for something, but if it's a lexer token or the state
of a SMTP connection or the like, I want to make it its own type to save
me from mistakes, even if I never define any methods for it. However,
if using my own types starts making it harder to use people's generics
implementations (because they've forgotten that '~
'), I'm being pushed
away from it.
Some of the mistakes of leaving out the '~
' will be found early, and I
think adding it wouldn't create API problems for existing users, so this
may not be a big issue in practice. But I wish that the defaults were
the other way around, so that you had to go out of your way to restrict
generics to specifically those types with no derived types allowed.
(If you just list some types without using a union element you've most likely just created an unsatisfiable generic type with an empty type set. However you're likely to notice this right away, since presumably you're going to try to use your generics, if only in tests.)
|
|