2022-02-12
The 'any' confusion in Go generics between type constraints and interfaces
Any system of generic types, such as Go will have in Go 1.18, needs some way to specify constraints on the specific types that generic code can take. Go uses what it calls "type sets", which reuse Go's existing interface types with some extensions. However, this reuse creates a potential for confusion, one that I've already seen come up in some articles about Go generics such as this one (via).
Suppose that you have some generic types and code (and interfaces):
type Result[T any] []Ttype Fredable[T any] interface { Fred(s string) T } type Barable interface { Bar() uint64 } type Addable interface { ~uint | ~float64 }
The Barable
type is an interface type that can be used today
in Go 1.17, but it's also usable as a generic type constraint in a
way that's different from just having a function that takes arguments
with the interface type:
func DoBar[T Barable](a, b T) uint64 { return a.Bar() + b.Bar() }
(Among other things, this generic function requires a
and b
to have
the same type when it's instantiated to a specific type.)
Now consider the following set of declarations using the Result
generic type that creates a slice of the given type:
var r1 Result[Barable] // okayvar r2 Result[Fredable] // error var r3 Result[Addable] // error var r4 Result[any] // okay. What?
The type of r1
is a slice of Barable
interface values. Barable
is a regular interface type and you can declare slices of interface
types, which contain interface values of that (interface) type. You
cannot declare r2
or r3
because although they are both declared
using the type
and interface
keywords, neither Fredable
nor
Addable
are normal interface types. They're only usable as type
constraints, and the Result
generic type needs a type, not a type
constraint.
The potentially confusing case is the last one, 'Result[any]
'. Right
now, 'any
' is new syntax that generally only shows up in articles
about Go generics, as a type non-constraint that means 'any type is
acceptable'. However, it's an alias for 'interface{}
', the universal
interface. Used in or as a type constraint, it means that there is no
constraint on the types that the generic type can be used with (you can
make a slice of anything). Used as a regular type, though, it means what
it usually means, so r4
is a slice of 'interface{}
' values (and
you'll be able to add anything to it, because anything can be converted
to an 'interface{}
' value).
My personal view is that it might be simpler if 'any
' was only
accepted in type constraints and couldn't be used as a regular interface
type. This is already the case with the 'comparable
' type constraint,
which doesn't map naturally to something in normal, non-generic Go. If
'any
' could only be used in type constraints, I think 'interface{}
'
should still be accepted as equivalent to 'any
' there. But I
understand why the Go developers did 'any
' this way, especially since
the type sets approach requires 'interface{}
' to be equivalent to it.
As a side note, because Fredable
takes a type parameter and can
be instantiated to become a specific type, we can do a version of
this with additional work. We can write:
var r5 Result[Fredable[string]]
However, there's no way to use Addable
this way. The Go compiler
error messages will tell us this, because we get different ones in
each case. Currently this is:
cannot use generic type Fredable[T any] without instantiation interface contains type constraints
(The error messages might change before Go 1.18 is released.)
The type of r5
is also different and more specific than you might
want, although there is a whole question of what 'Result[Addable]
'
or 'Result[Fredable]
' would really mean if Go accepted it. A full
discussion of that is for another entry.