Chris's Wiki :: blog/programming/GoGenericsTypeSets


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 is T
    • 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.)