The 'any' confusion in Go generics between type constraints and interfaces

February 12, 2022

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] []T
type 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]  // okay
var 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.


Comments on this page:

My hope is that it this limitation will be lifted in Go 1.18+ and constraints can be used as interfaces. That would bring tagged unions to Go, which has been a long term requested feature.

So, as someone who has only watched all this from the sidelines… does this work?

var r5 Result[Fredable[any]]

And if so, what does it do – particularly if Fredable has another function?

type Fredable[T any] interface {
   Fred(s string) T
   Barney(i int) T
}
By cks at 2022-02-13 15:02:59:

Your example works. 'Fredable[any]' means to instantiate the type constraint Fredable with interface{} as the concrete type T, so what we get is a normal, non-generic (interface) type that is:

type <something> interface {
  Fred(s string) interface{}
  Barney(i int) interface{}
}

(Go 1.18 has a specific name for the <something>, which I'm eliding because it would confuse things a bit.)

Instantiating the generic type Result with this gives us a '[]<something>' type, which is a slice of interface values. Anything you put in this slice must be convertible to the interface (or it can be nil); if you try to put a mismatched thing in, it will fail with an appropriate error at compile time.

Here's a version of this on the Go tip playground, which sort of illustrates what's going on. Doing a better job would take using the reflect package, which is too much work.

This looks confusing partly because 'any' is both a type non-constraint in generic type sets and another name for 'interface{}', the universal interface, in non-generic code. When we say 'Fredable[any]', we're using it in the non-generic sense where it's the same as 'interface{}', although it may look like we're using it as a type non-constraint.

Thanks. In itself the fact that any is just an alias for interface{} was clear enough. What hadn’t fully sunk in is that in the given scenario, the types of the values returned by Fred and Barney are not in any way constrained to match, other than that they must individually satisfy interface{}. Essentially a type constraint is a constraint on the programmer (restricting what types they can choose to instantiate a concrete type from a generic one) only – purely static/textual and entirely absent from the picture come runtime (unlike types themselves).

Written on 12 February 2022.
« Some things on strict and relaxed DKIM alignment in DMARC
Go generics: the question of types made from generic types and type sets »

Page tools: View Source, View Normal, Add Comment.
Search:
Login: Password:
Atom Syndication: Recent Comments.

Last modified: Sat Feb 12 22:14:44 2022
This dinky wiki is brought to you by the Insane Hackers Guild, Python sub-branch.