golang/go

spec: add generic programming using type parameters

ianlancetaylor opened this issue Β· 453 comments

We propose adding support for type parameters to Go. This will change the Go language to support a form of generic programming.

A detailed proposal document has been published, with input from many members of the Go community. We are now taking the next step and proposing that this document become a part of the language.

A very high level overview of the proposed changes:

  • Functions can have an additional type parameter list that uses square brackets but otherwise looks like an ordinary parameter list: func F[T any](p T) { ... }.
  • These type parameters can be used by the regular parameters and in the function body.
  • Types can also have a type parameter list: type MySlice[T any] []T.
  • Each type parameter has a type constraint, just as each ordinary parameter has a type: func F[T Constraint](p T) { ... }.
  • Type constraints are interface types.
  • The new predeclared name any is a type constraint that permits any type.
  • 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.
  • Using a generic function or type requires passing type arguments.
  • Type inference permits omitting the type arguments of a function call in common cases.

For more background on this proposal, see the recent blog post.

In the discussion on this issue, we invite substantive criticisms and comments, but please try to avoid repeating earlier comments, and please try to avoid simple plus-one and minus-one comments. Instead, add thumbs-up/thumbs-down emoji reactions to comments with which you agree or disagree, or to the proposal as a whole.

If you don't understand parts of the design please consider asking questions in a forum, rather than on this issue, to keep the discussion here more focused. See https://golang.org/wiki/Questions.

Why any and not interface{} ?

@atdiar This is explained in the proposal:

However, itβ€˜s tedious to have to write interface{} every time you write a generic function that doesn’t impose constraints on its type parameters. So in this design we suggest a type constraint any that is equivalent to interface{}.

Please read it in full before asking questions about it.

Yes sorry, I just checked the link and was about to delete my message.
Would still prefer to have the empty interface. How tedious can this really be? Especially since we can define type aliases ourselves...
Other than that, I'm fine with it.

Will this proposal cover channels as well (e.g. func F[T any](p chan T) { ... })?

tsal commented

Why any and not interface{} ?

an interface{} wouldn't make sense here since we're describing a generic trait that needs to be implemented. This is more meta-code and interface{} is still concrete - even if it is "generic" in some senses of the word.

I'm also having trouble thinking how you could implement any interface{} generic parameters, since you don't know what the interface will actually be - and if you're doing interface type-checking here, it's defeating the entire point of generics (IMO).

Will this proposal cover channels as well (e.g. func F[T any](p chan T) { ... })?

Yes, it seems to cover that.

I remain concerned that this proposal overloads words (and keywords!) that formerly had very clear meanings β€” specifically the words type and interface and their corresponding keywords β€” such that they each now refer to two mostly-distinct concepts that really ought to instead have their own names. (I wrote up this concern in much more detail last summer, at https://github.com/bcmills/go2go/blob/master/typelist.md.)


Specifically, the word type today is defined as:

A type determines a set of values together with operations and methods specific to those values.

Under this proposal, I believe that a type would instead be either a set of values with operations, or a set of sets of values, each with its own set of operations.

And today the word interface, in the context of Go, refers to a type, such that:

A variable of interface type can store a value of any type with a method set that is any superset of the interface.

Under this proposal, a variable of interface type can store a value of any type with a method set that is any superset of the interface, unless that interface type refers to a set of sets of values, in which case no such variable can be declared.


I'd like to see more detail on the exact wording proposed for the spec, but for now I am against this specific design, on the grounds that the ad-hoc overloading of terms is both confusing, and avoidable with relatively small changes in syntax and specification.

func foo[T Stringer](t T) string {
	return t.String()
}
func foo(t Stringer) string {
	return t.String()
}

The difference are very subtil. How will you document the best practice when a Go1 interface is enough ?
I mean, how to prevent abuse of generic when both can be used ?

Would still prefer to have the empty interface. How tedious can this really be? Especially since we can define type aliases ourselves...

I think interface{} is a mistake. It is a hack to permit something like void * without any semantic cues to help a user understand what is going on. I would have preferred to have "any" as a type in the language from the beginning, even it it was just an alias for interface{} under the covers.

Of course, the new "any" is different than interface{}. It would be nice to have a named type that means "any type by reference" instead of "any type by substitution".

```go
func foo[T Stringer](t T) string {
	return t.String()
}
func foo(t Stringer) string {
	return t.String()
}

The difference are very subtil. How will you document the best practice when a Go1 interface is enough ?
I mean, how to prevent abuse of generic when both can be used ?

Valid point, in your example, an interface would be the better choice. However, a more apt use case of generics would be sort. Operations on slices of arbitrary types would be distinctly less verbose with generics as compared to interfaces. This talk touches on some of the points: blog.golang.org/why-generics (disclaimer: syntax in this link is different than this generics proposal, but the points are useful to draw comparison)

What are the plans for amending the standard library to utilize generics? I see two important tracts of work here.

  1. retroactively applying generics to packages like sort, container/list
  2. Creating new packages and libraries that were previously cumbersome without generics; i.e. mathematical set functions,

@bcmills

Specifically, the word type today is defined as:

A type determines a set of values together with operations and methods specific to those values.

That sentence describes a struct. An interface is also a kind of type in Go, and it does not determine the set of values or operations that are present, only the set of methods.

The fact that a type is not just a struct is why the syntax in the language is type foo interface { ... }, type foo struct { ... }, and even type foo bar or type foo = bar.

A generic type is just as concretely a set of values, operations, and methods as an interface is (which is to say, you an argue that it isn't). So either we should redefine interface to not be a type (by the definition you're quoting), or we should accept that a generic type is also a type, just one that requires type parameters to be resolved before it becomes a concrete type.

If the proposal is misusing the term "type" in place of "type parameter" anywhere, I think that could be valid criticism... but it sounds like you're criticizing some ambiguous/arguably wrong terminology that exists in the Go language spec, which is terminology that is refuted by the language itself, as demonstrated by Go syntax above. If an interface is not a type, we should not prefix the declaration with the word type, but we do.

That whole area of discussion seems off topic here, and clarifications to the existing language spec could be proposed somewhere else? I've read through the generic proposal several times and I haven't come away feeling like the terminology used was ambiguous or confusing, and your statements here do not effectively make the case for that either, in my opinion.

fzipp commented

Of course, the new "any" is different than interface{}.

The new "any" is not different from interface{} as a type constraint. [T any] and [T interface{}] are interchangeable as per the proposal.

What are the plans for amending the standard library to utilize generics? I see two important tracts of work here.

1. retroactively applying generics to packages like `sort`, `container/list`

2. Creating new packages and libraries that were previously cumbersome without generics; i.e. mathematical set functions,

Yes, and after generics are implemented, I hope the container package will be expanded to include other common data structures present in c++/java std libs

@bcmills

Specifically, the word type today is defined as:
A type determines a set of values together with operations and methods specific to those values.

That sentence describes a struct. An interface is also a kind of type in Go, and it does not determine the set of values or operations that are present, only the set of methods.

The interface type definition specifies the methods (which are operations) present on values of that interface type. Because values must implement the interface to be used as that interface type, the interface type does indeed determine a set of values (always a superset of other types').

fzipp commented
func foo[T Stringer](t T) string {
	return t.String()
}
func foo(t Stringer) string {
	return t.String()
}

The difference are very subtil. How will you document the best practice when a Go1 interface is enough ?
I mean, how to prevent abuse of generic when both can be used ?

I'd expect a linter warning: "useless use of type parameter"

@bcmills

Specifically, the word type today is defined as:
A type determines a set of values together with operations and methods specific to those values.

That sentence describes a struct. An interface is also a kind of type in Go, and it does not determine the set of values or operations that are present, only the set of methods.

The interface type definition specifies the methods (which are operations) present on values of that interface type. Because values must implement the interface to be used as that interface type, the interface type does indeed determine a set of values (always a superset of other types').

If you want to go down that route... the same exact thing applies to the "overloading" of "type" to refer to generic types as well. In order for a value to be substituted for a type parameter, it must implement the interface constraints, and to do that, it must be a concrete type. Therefore, a generic type "does indeed determine a set of values (always a superset of other types)".

It's the same thing. Either an interface is a type (in which case, it's fine for the proposal to use its current terminology), or it's not (in which case it's not okay for the Go language to define interfaces as types).

Either way, someone could propose that the Go language spec is written in a confusing way in the quoted section, but it wouldn't change any outcomes regarding this proposal or the current-day reality of Go.

Is this proposal related to ongoing development on the dev.go2go branch?

@bcmills

Specifically, the word type today is defined as:
A type determines a set of values together with operations and methods specific to those values.

That sentence describes a struct. An interface is also a kind of type in Go, and it does not determine the set of values or operations that are present, only the set of methods.

The interface type definition specifies the methods (which are operations) present on values of that interface type. Because values must implement the interface to be used as that interface type, the interface type does indeed determine a set of values (always a superset of other types').

In general, methods in Go are tied to type definitions and behave, in a lot circumstances, like any other function except that there's an argument placed before the function name. That's why, unlike most languages, Go allows you to call a method on a nil pointer no problem, meaning that you can handle the nil pointer case in the method itself instead of elsewhere.

Interfaces are a strange exception to this. Despite the fact that a type is defined, as in type Example interface { /* ... */ }, attempting to declare methods on that type will fail, purely because the underlying type is of kind interface. This dichotomy has always existed in Go, and it's always kind of bugged me, but it's a very minor thing that's basically never any kind of problem in practice, and I don't really think that the usage of interfaces in this proposal changes that much at all.

@p-kraszewski The active development is currently on dev.typeparams.

knz commented

Is it possible to create a chan T when T has constraint any? I did not find mention of channels in the section "Operations permitted for any type".

@p-kraszewski The most up-to-date development is happening on the dev.typeparams branch. The dev.go2go branch was used to develop a prototype and the go2go playground; general development of that has been suspended in favor of a real implementation in dev.typeparams. But we hope to update dev.go2go occasionally to keep the go2go playground in reasonably good shape.

Is it possible to create a chan T when T has constraint any? I did not find mention of channels in the section "Operations permitted for any type".

I think that you're confusing the declaration of a type parameter and the usage. The parameters are declared in function and type declarations and are essentially scoped to those functions and types. For contrived example,

//    declaration       usages
//        v             v    v
func Send[T any](c chan T, v T) {
  c <- v
}

Once they're declared, there basically isn't any difference in terms of usage between the type parameters and any other type, so they can be used as the element type of a channel, or the element type of a slice, or an argument to a function, or basically anything else.

How can I use this with clojures? Both for func's that take a generic function as a param, or have one as a return type:

ie:

func Print[T any](s []T) {
	for _, v := range s {
		fmt.Print(v)
	}
}

func FuncGen[T any]() func(s []T) {
	return Print[T]
}

func main() {
	FuncGen()([]string{"Hello, ", "playground\n"})
}
knz commented

The section "Operations permitted for any type" in the spec does not seem to suggest I can create a chan T if T has constraint any. That is, under the current text of that section (I did not look at the examples), the following code is invalid:

func foo[T any]() {
  c := make(chan T)
}

So my question is: is the code valid? (And the section incomplete?) Or is the section text valid? (And the code example here invalid?)

func foo[T Stringer](t T) string {
	return t.String()
}
func foo(t Stringer) string {
	return t.String()
}

The difference are very subtil. How will you document the best practice when a Go1 interface is enough ?
I mean, how to prevent abuse of generic when both can be used ?

govet or golint could help with that.

func foo[T Stringer](t T) string {
	return t.String()
}
func foo(t Stringer) string {
	return t.String()
}

The difference are very subtil. How will you document the best practice when a Go1 interface is enough ?
I mean, how to prevent abuse of generic when both can be used ?

I'd expect a linter warning: "useless use of type parameter"

@fzipp That wouldn't be entirely accurate as a warning. Depending on the compiler's devirtualization pass, the two would have different performance characteristics. Assuming the "stenciling" approach for implementing generics, each version of foo would be expanded out to each specific type to generate the most efficient code, and the former would be faster as the compiler knows exactly what String to call, while in the latter the compiler may potentially box the value into an interface type and then have to look up the correct String for the type it gets.

@knz I suggest you use the go2go playground to explore your examples, which work fine. The section "Operations permitted for any type" talks about values of type parameter type, not values of types that contain type parameters (such as chan T, where T is a type parameter). Also, what we have so far is a fairly precise proposal, but it's not a specification yet; please keep that in mind. Thanks.

The section "Operations permitted for any type" in the spec does not seem to suggest I can create a chan T if T has constraint any. That is, under the current text of that section (I did not look at the examples), the following code is invalid:

func foo[T any]() {
  c := make(chan T)
}

So my question is: is the code valid? (And the section incomplete?) Or is the section text valid? (And the code example here invalid?)

Your code is valid and the text is valid. The text says

define and use composite types that use those types, such as a slice of that type

That includes channels. The only place that an any constrained type wouldn't work is in the key for a map, which requires that the type be comparable via ==.

fzipp commented
func foo[T Stringer](t T) string {
	return t.String()
}
func foo(t Stringer) string {
	return t.String()
}

The difference are very subtil. How will you document the best practice when a Go1 interface is enough ?
I mean, how to prevent abuse of generic when both can be used ?

I'd expect a linter warning: "useless use of type parameter"

@fzipp That wouldn't be entirely accurate as a warning. Depending on the compiler's devirtualization pass, the two would have different performance characteristics. Assuming the "stenciling" approach for implementing generics, each version of foo would be expanded out to each specific type to generate the most efficient code, and the former would be faster as the compiler knows exactly what String to call, while in the latter the compiler may potentially box the value into an interface type and then have to look up the correct String for the type it gets.

I don't think the programmer should make the optimisation choice here, but the compiler. The compiler could potentially choose to stencil the regular interface as well. Different compilers may also compile type parameters differently.

Can we just omit any? Seems like a useless appendage.

func Print[T](s []T) {
    for _, v := range s {
        fmt.Print(v)
    }
}
type Stringer interface {
    String() string
}

func Print[T Stringer](s []T) {
    for _, v := range s {
        fmt.Print(v)
    }
}

The compiler could potentially choose to stencil the regular interface as well.

This is what the devirtualization pass does for specific method calls whose type is known, yes, though not to the same extent in the above case; the above case is OK as the function is also inlined (another compiler detail), so then the type used is visible. I don't think any reasonable compiler is going in the latter case for each T emit a foo method to be used and then decide which one to pick (as that would blow up). Generics make which ones need to exist explicit; the developer is asking for this to occur.

I just wanted to point out that the two are in fact different and would be approached differently, and that doing so is likely not going to be a good lint.

Related, but IIRC in a mailing list or some thread, it was mentioned that a package may actually have unused type params to have a uniform API and it was intended behavior, but I can't recall where off the top of my head...

Can we just omit any? Seems like a useless appendage.

There was quite a bit of debate about this already. It started because the original version of the draft required the keyword type in the parameter list for parsing reasons, such as type List[type T] struct { /* ... */ }, but it was only actually necessary if there was a type parameter with no constraints. As an alternative, it was decided to simply require all parameters to have constraints, but requiring interface{} for everything that didn't have any actual constraints was very awkward and read weirdly, plus potentially being misleading because it actually has a fairly different meaning from interface{} used elsewhere. Someone unrelatedly suggested a global any alias for interface{}, and that suggestion got co-opted for usage here, thus cleaning it up a bit.

There was debate about making the alias global, instead of only for usage in constraints, but that was a lot more controversial and it could always be done later if necessary anyways, so it was decided to just do this at least for now.

@jeffreydwalter What @DeedleFake said.

To elaborate a bit more:

For regularity and simplicity, type parameter lists essentially follow the syntax of regular parameter lists (but type parameters are enclosed using square brackets). Additionally, the current syntax for type parameters requires that all type parameters have names (incl. '_') and a constraint, and that a type parameter list is not empty. This has been discussed at length in prior designs. This approach avoids a significant number of problems with parsing ambiguities and simplifies the design.

Here is a simple example to illustrate the problem: If you could leave away the constraint (any in most cases), it's not clear how this declaration should be parsed.

type A[N] T

Is this an array declaration (where N might be a constant declared in a file belonging to the same package that's not been parsed yet), or is it a generic type with type parameter N which is not used?

There's a whole zoo of syntactic problems which are neatly resolved by simply following the regular parameter list syntax (which requires that there's always a type, or in this case, a constraint).

I think this part of the design (syntax of type parameter lists) is very much settled at this point.

hmage commented

Ken Thompson's quote:

When the three of us [Thompson, Rob Pike, and Robert Griesemer] got started, it was pure research. The three of us got together and decided that we hated C++. [laughter] ... [Returning to Go,] we started off with the idea that all three of us had to be talked into every feature in the language, so there was no extraneous garbage put into the language for any reason.

Did you talk to all three of them before this feature got greenlighted?

I'm very interested to see reaction of Rob Pike, Ken Thompson and Robert Griesemer about generics in golang, and if they support the idea.

There is one difference between the parsing of the type parameter list and the normal argument list that I think should be noted, and that is that an argument list doesn't require names. So long as none of the arguments are named, the list can be just types. This allows for function type definitions that don't specify names for their arguments, as well as allowing the list of returns for a function to not require names. Because of the parsing ambiguities, that's not true for type parameters, though.

@steeling Your code looks right to me, except that you would have to pass a type argument when you call FuncGen.

General comment: If you are new to this discussion (as in "I haven't followed generics development in Go at all until now"), please refrain from "shooting from the hip" with immediate questions. Please take the time to read the design carefully as it may answer a lot of open questions (yes, I know it's long). Then, use the go2go playground to explore your questions further. Follow the discussions on this issue for a couple of days before pulling the trigger. Thanks.

I think this will help tremendously in keeping this issue manageable in size, and in turn followable for other people.

Please also consider asking questions in a forum, rather than on this issue. See https://golang.org/wiki/Questions. Thanks.

@hmage Rob and Ken are very aware of this proposal. Robert co-authored the proposal and has been responding on this issue.

@coder543

If the proposal is misusing the term "type" in place of "type parameter" anywhere, I think that could be valid criticism... but it sounds like you're criticizing some ambiguous/arguably wrong terminology that exists in the Go language spec,

I think you have misunderstood my critique. The proposal is not misusing the term β€œtype” in place of β€œtype parameter”. The proposal is misusing the term β€œtype” in place of type constraint. In the existing specification, an β€œinterface type” is a type. I am arguing that the proposal requires that definition of β€œtype” to change to a different definition that is not particularly coherent.

(Did you read the writeup that I had linked?)

It's the same thing. Either an interface is a type (in which case, it's fine for the proposal to use its current terminology), or it's not (in which case it's not okay for the Go language to define interfaces as types).

An β€œinterface type” as defined by the spec today is a β€œtype” as defined by the spec today.

Presumably, an β€œinterface type” as used in the proposal should also be a β€œtype” as defined in the proposal. However, an β€œinterface type” as used in the proposal is not a β€œtype” as defined today (because it may instead be a type-list constraint), and the proposal does not provide an updated definition for the word β€œtype”.

@zephyrtronium

The interface type definition specifies the methods (which are operations) present on values of that interface type. Because values must implement the interface to be used as that interface type, the interface type does indeed determine a set of values (always a superset of other types').

That is true for interfaces as defined today, because a method is the same operation (a method call) for all values that implement the interface.

However, it is not true for type-list interfaces as defined in this proposal. The types string and int both β€œimplement” interface { type string, int }, but a program cannot invoke the + operation abstractly on a value of type interface { type string, int } because the + operation is not the same operation for both types.

That is: an interface type today defines one set of values (the values that have the named method(s)), and one set of operations (the declared methods), whereas the interface types in this proposal define multiple sets of operations (the supported operations per underlying type).

Of course, the new "any" is different than interface{}.

The new "any" is not different from interface{} as a type constraint. [T any] and [T interface{}] are interchangeable as per the proposal.

Sure. But: func do_something[T any](v T) is not the same as func do_something(v interface{}). And that's my point. It would be nice to be able to say func do_something(v any).

I feel like that would be a unifying change because then when used in [T any] it would mean substitution and when used in (v any) it would mean reference. :-)

I'm a little concerned about type lists in interfaces because this seems to cause a disconnect between primitive types and user-created types. For example, the proposal gives an example of a generic sort function sort.OrderedSlice, which could take values that satisfy the Ordered interface, i.e. various primitive types where the < operator is available. But it seems like we would need a second function such as sort.LesserSlice to handle user-defined types that satisfy:

type Lesser[T any] interface {
    Less(T) bool
}

Instead of sorting, another example could be a generic minimum function. In any case, I think this means that code that relies on types to generically satisfy operators like <, >, ==, etc... would require two definitions: a "basic" one for primitive types that uses a type list to enforce the constraint, and a more general one for user-defined types that uses a method set to enforce the constraint. Please correct me if this is in fact not the case.

I'm not sure what the right approach is to solve the problem of operators in generic functions (maybe type lists are good enough). I believe some other languages rely on operator overloading, which is unnacceptable for Go (for example Python uses __eq__, __gt__, __lt__...). I wonder if it would be possible to define all built-in operators also as functions on the primitive types. Then, for example, int would satisfy the Lesser interface because it would have a function Less(int) bool defined for it that is equivalent to the < operator. Each primitive type would need a function for every built-in operator it supports. Then we can think of built-in operators as syntax sugar for calling the explicit Less, Equal, Greater... functions. I believe then it would also be possible to write code like 1.Less(2), which would be equivalent to 1 < 2. Functions that use these operators on generic values would need to use the explicit syntax, but I don't think that is a big deal. With this change, type lists are no longer necessary. I think this could also remove the need for comparable, although some more thought is necessary there (since this deals with operators that are defined by default on user types).

This change is similar to operator overloading, but is much more restrictive because it still only allows operators to be used with primitive types (defining a Less function on a custom type would not make it usable with < -- this sugar is reserved for primitive types only).

@nadiasvertex any is predeclared as type any interface{} (it could be an alias, it doesn't matter because it's an interface). There is no strong reason for not making any generally available except that we believe that that should be a separate decision once we accept generics. See also #33232. For now, any is simply only visible (in scope) in constraint position. It would be trivial to remove this restriction.

Regarding your comment on any meaning "substitution" or "reference" depending in use: any just means interface{}, nothing more, nothing less. But a type parameter list is instantiated (through substitution) at compile time; an ordinary parameter list is assigned to (via function invocation) at run time.

func foo[T Stringer](t T) string {
	return t.String()
}
func foo(t Stringer) string {
	return t.String()
}

The difference are very subtil. How will you document the best practice when a Go1 interface is enough ?
I mean, how to prevent abuse of generic when both can be used ?

I'd expect a linter warning: "useless use of type parameter"

It wouldn't be useless, since there's still a runtime penalty for using interfaces, that's one of the reasons we (go users, not the go team) wanted generics to begin with.

Here is a simple example to illustrate the problem: If you could leave away the constraint (any in most cases), it's not clear how this declaration should be parsed.

type A[N] T

Is this an array declaration (where N might be a constant declared in a file belonging to the same package that's not been parsed yet), or is it a generic type with type parameter N which is not used?

I'm not suggesting leaving out the [T] declaration, so in your example, I'd expect that to be parsed as an array of N of type A with the label T.

I would expect the correct generic declaration to be: type A[T][N] T or type A[T Stringer][N] T.

If type a[] string was invalid syntax in favor of type a []string then the ambiguity of type A[N] T goes away and type A[T] [N]T is valid and easily parsable.

Edit: I see where the issue is with this syntax in this case. Why [] over <>? Perhaps type A<T>[N] T would be better.

https://go.googlesource.com/proposal/+/refs/heads/master/design/go2draft-type-parameters.md#why-not-use-the-syntax-like-c_and-java

Seriously, everyone, please actually read the proposal before responding with suggestions.

I think there is a subset of the community which is concerned about generics damaging the simplicity that some of us love in Go. Specifically the tendency towards concrete, direct, unwrapped types, and the concise and specific API design that I associate with the language.

I'm not necessarily avowing this risk, but I think it would be helpful for me and others if you could address this concern directly. If you read the Hacker News comments on this proposal, you will see this concern in spades, and my impression is that this is the primary resistance to generics in the Go community. I know this is a tired topic, but I think given the formal proposal, it's worth addressing.

Do you think generics will increase the complexity of the language or the typical Go program in a problematic way? What can be done to minimize this?

jorng commented

In the section describing Composite types in constraints, there is an example about composite struct types with shared fields (and requiring that the field type is the same).

Maybe I've missed it from past discussions (apologies in advance, if so), but is there any interest in enabling constraints for struct fields? Currently it appears the only available constraints for a struct are the same as interfaces are right now: methods. Seems to me it would be super useful to be able to define a constraint on a shared field of a struct, so that any struct that has that field exported would satisfy the constraint.

Among the many possible uses, one example would be for structs representing database entries that may have an UpdatedAt time.Time field.

type timeUpdater interface {
        .UpdatedAt time.Time
}

func UpdateTime[T timeUpdater](v T) {
        t.UpdatedAt = time.Now()
}

I would generally be okay with this design.

One thing I would like to put out there, though, is the possibility of splitting off type-lists (and thus operators in generic functions) as a separate step. IMO type-parameters itself (constrained only by traditional interfaces) already add quite a lot of expressive power on their own and type-lists seem to factor out well as a natural extension of that. It would allow to at least evaluate the reaction to generics and what people use them for, before putting in the entire design as a whole.

And to be clear: @ianlancetaylor has said many times that he considers a form of operator-usage for generic functions non-optional, so I don't expect this to be accepted :) But even if operators in generic functions are non-optional, I don't see a reason why the complete design must be implemented in a single release. And if a language extension can be naturally split up like this, I would generally argue for a conservative approach.

My one worry here, which I think has been echoed in other comments like #43651 (comment) and #43651 (comment), is that this proposal does seem to allow wide latitude for coders to use generics where the standard go1 interfaces may be more reasonable or simple.

In the section on Complexity you state that "the increased complexity is small for people reading well written generic code". I wonder if maybe you could dive into any go vet features that will be added as part of generics, that may prevent coders from doing things like described in #43651 (comment) ?

In other words, could the design of generics include some discussion of go vet features that may encourage go coders to write "well written generic code"?

@jorng

is there any interest in enabling constraints for struct fields?

Field-matching could be added orthogonally, by allowing interface types to declare struct fields as proposed in #23796 (which was, as best as I can tell, retracted due to lack of strong interest).


Ironically, @griesemer replied to that proposal (in #23796 (comment)) with:

I am not a fan as it destroys the notion of an interface being an abstract type which doesn't imply a specific implementation.

But arguably the same can be said of type-list interfaces in this proposal, which explicitly do imply a specific implementation.

jfcg commented

Hi,
Huge thanks for the proposal. It enables to combine sorting functions for Ordered basic types in sorty into a single template function like:

func Sort[T Ordered](s []T) {
    ...
}

However there is a subtle detail: sorty is a concurrent quicksort implementation which (like many other quicksort implementations) falls back to a (modified) insertionsort at a certain range size limit called Mli (max length for insertion sort).

sorty gains a lot of performance (%15+) by tuning relevant parameters (especially Mli). The emprical tuned value of Mli turns out to be around:

  • 108 for arithmetic basic types (integers, floats)
  • 36 for string

on modern CPUs. This is a huge difference that cannot be ignored. So basically, I need/want to write something like this:

var Mli[T Ordered] int = 108 // or constant
var Mli[T string]  int =  36 // specialize for string

and use Mli[T] inside Sort[T Ordered]. This is type-parametrized variables/constants. Without this, this proposal will not achieve its true potential.

Thanks..

fzipp commented
func foo[T Stringer](t T) string {
	return t.String()
}
func foo(t Stringer) string {
	return t.String()
}

I'd expect a linter warning: "useless use of type parameter"

It wouldn't be useless, since there's still a runtime penalty for using interfaces, that's one of the reasons we (go users, not the go team) wanted generics to begin with.

Ideally both of the above variants would compile to the same machine code. It would be sad if people started littering their code with (from a type checking perspective unnecessary) type parameters solely for perceived performance reasons.

...the possibility of splitting off type-lists (and thus operators in generic functions) as a separate step.

I'm of the same opinion as @Merovius. I'm really happy with this proposal, but I wonder if we're painting ourselves into a corner with the design of type lists (especially given #41716). People are currently used to implementing ad-hoc interfaces for named types, and I could imagine them doing the same if they wanted to use a generic Min without an Ordered constraint.

type MyInt int

func (a MyInt) Less(b MyInt) bool { return a < b }

func example() {
	Min([]MyInt{2,4,1})
}

The current draft already allows us to write the above, where Min has a signature func Min[T interface { Less(T) bool }](s []T) T.

@jfcg I think that what you are describing is not just type-parameterized variables, but also specialization, such that a particular instantiation of the variable is selected based on the type argument. The current design does not support any form of specialization. That is an intentional choice to reduce complexity.

@ianlancetaylor @jfcg I was going to suggest doing something like this (I might be reading into the numbers, but it looks to be what's happening here), but was surprised to find out that you can't use unsafe.Sizeof on variables of type parameter type. Is there a reason for that (beyond that it's hard to talk about what that would mean in terms of being a compile-time constant)?

[edit] well, this works, but surprisingly, it gives different numbers than the ones you mention. It's still suspicious that the string number is exactly 3x the int number… :)

jorng commented

Also, I'm certain I saw this discussed in previous threads, but my one huge nit with the proposal is the re-use / overloading of the interface keyword. Seems like the primary purpose is to avoid adding a new keyword, with maybe the desirable side-effect of reusing existing pre-defined interfaces.

I would much prefer that constraints actually use a new keyword. Preferably: constraint.

Also, I'm certain I saw this discussed in previous threads, but my one huge nit with the proposal is the re-use / overloading of the interface keyword. Seems like the primary purpose is to avoid adding a new keyword, with maybe the desirable side-effect of reusing existing pre-defined interfaces.

I would much prefer that constraints actually use a new keyword. Preferably: constraint.

This was already suggested in the original generics proposal (was called contract then) and is why we're here now without it. The constraint keyword suggests that it's doing something different to an interface, but in reality it isn't. It's just the same thing. An interface is a constraint which decides what types are acceptable to be passed as a parameter right now in Go. Generics just add that little bit of needed functionality which let us constrain our parameters with an interface but also know the underlying type of the parameter.

I don't really see why we'd ever need a new keyword, and that was the general consensus anyway. So it almost certainly won't change now.

jorng commented

This was already suggested in the original generics proposal and is why we're here now without it. The constraint keyword suggests that it's doing something different to an interface, but in reality it isn't. It's just the same thing.

That was before the idea of composite type in constraints, though... wasn't it? Now they are different things. One cannot use a constraint with composite types as a traditional interface.

@jfcg You can use the existing reflection mechanisms to handle specialization. In my opinion, this is exactly the sort of thing that reflect handles well today, and we don't need to duplicate with the new generics mechanism. If this is a common pattern, it might be worth adding some intelligence to the compiler to make reflect.TypeOf() be resolved at compile time for these sort of use cases.

func IterationLimit[T any]() int {
	var t T
	kind := reflect.TypeOf(t).Kind()
	switch kind {
	case reflect.String:
		return 36
	default:
		return 108
	}
}

@HALtheWise

There no reason to use reflect package, type switch is enough.
Go compiler is smart enough to delete unnecessary interface{} downcast and determine the type at compile time in code generated by go2go.

func IterationLimit[T any]() int {
	var t T
	switch (interface{})(t).(type) {
	case string:
		return 36
	default:
		return 108
	}
}

Do you think generics will increase the complexity of the language or the typical Go program in a problematic way? What can be done to minimize this?

@hherman1 I am less concerned about the increase of language complexity (*), and more concerned about what people do with generics. Type parameters introduce abstractions, and needless abstraction makes code hard to read.

Similar to what happened in the early days of Go where every piece of Go code used channels and goroutines because one could, I suspect the same will happen with generic features, once available. Certainly, some of the playground examples I have seen were overly complex: not too hard to write but quite hard to read due to undue abstraction.

We will need to establish best practices for good generics use. Generic features have their place, and sometimes they are exactly what is needed. But they are just one more tool in the tool set of Go. A move to write everything in generic fashion would be ill-conceived.

(*) Ignoring type lists in interfaces for a moment, the actual language changes are very small: type and function declarations can have an additional (type) parameter list. Type parameters are a new kind of type, but still just a type. These changes are fully orthogonal to the rest of the language and well understood. It is this simplicity and orthogonality which will enable powerful (and possibly hard-to-understand) uses. Type lists add the most complexity simply because they deal with the irregularity of Go's basic types and operations, but they also enable the kind of code that is really hard to write now and cannot easily be emulated with interfaces and methods alone.

Similar to what happened in the early days of Go where every piece of Go code used channels and goroutines because one could, I suspect the same will happen with generic features, once available. Certainly, some of the playground examples I have seen were overly complex: not too hard to write but quite hard to read due to undue abstraction.

@griesemer are there perhaps some go vet features being considered that could help with this? and if so, could they be formalized as part of the proposal?

@tdakkota, that does something slightly different from the reflect-based version. In your version, the use of a user-defined type Example string will not match the string branch, whereas in the reflect one it will.

Edit: Here's a playground example.

One feature that I'd like to see that was unfortunately lost when the change from contract to interface was made is the ability to specify generic convertability. For example, there's no way to say that you want a type A that can be converted to type B without knowing in advance what both A and B are. In other words, there's no way to write this contrived function:

func Convert[A, B NoConstraintIsPossibleHere](a A) B {
  return B(a)
}

It's possible that this could be solved similarly to comparable by adding a convertible constraint that takes two types itself:

func Convert[A, B convertible[B, A]](a A) B {
  return B(a)
}

@DeedleFake I think newtype type should not be handled same as its underlying type.
It should be converted to string explicitly or define a constraint implementation.

For example.

type IterationLimiter interface {
        IterationLimit() int
}

func IterationLimit[T interface{ type int, string, IterationLimiter }]() int {

In that case Example can have a IterationLimit() int method.

func foo[T Stringer](t T) string {
	return t.String()
}
func foo(t Stringer) string {
	return t.String()
}

The difference are very subtil. How will you document the best practice when a Go1 interface is enough ?
I mean, how to prevent abuse of generic when both can be used ?

We will need to establish best practices for good generics use.

Is it too heavy handed to formalize the best practice into a tool? gofmt -s already rewrites

for i, _ := range slice { }

into

for i := range slice { }

which is enforced by existing linters such as https://goreportcard.com/. If you do not run your code through gofmt -s it is flagged as a problem, which ensures that codebases follow the same style (even though for i, _ := range slice is also valid).

Similarly, gofmt -s could rewrite

func foo[T Stringer](t T) string

into

func foo(t Stringer) string

as well. That behaviour can then be reinforced by various linters, ensuring that we do not see a proliferation of func foo[T Stringer](t T) string when func foo(t Stringer) string will suffice.

@bokwoon95, gofmt makes only syntactic changes, not semantic ones. Those two declarations are semantically different.

For example, if T is instantiated with a concrete T that is itself an interface type, and the body of foo calls reflect.ValueOf(&t), then the returned reflect.Value will wrap a value of type *T in the first case but *Stringer in the second.

gofmt makes only syntactic changes, not semantic ones.

That's true, also unsaid in my suggestion is that if the semantics were to ever not match, gofmt should not rewrite. For example, this should obviously not be rewritten:

func foo[T Stringer](t T) T {
    return T
}

Your example of reflect.ValueOf falls under this category. But I agree that since gofmt is concerned with purely syntactical analysis, rewrites requiring semantic analysis should not be done under gofmt. Perhaps some other (officially sanctioned) tool.

@bokwoon95 I think such analysis are harder than you (and others in this thread who call for such a tool) realize.
There's also the obvious issue that as long as we don't know what people want to do, we can't write a tool to find that. If we could foresee all the uses of generics, the discussion would be much easier and we could change the design accordingly.

komuw commented

The design document notes;

In this design we introduce only two new predeclared names, `comparable` and `any`...

My nitpick comment is that the design shouldn't introduce these two predeclared names. The design would still stand without resting on those two.
Indeed the document also says;

We expect that if people find such names useful,    
we can introduce a package `constraints` that defines those names...

and;

this constraint(`Ordered`) would likely be defined and exported in a new std library package, `constraints`

The design is silent(unless I missed it) on why comparable needs to be predeclared but Ordered doesn't.
And thus it looks to me like all these three; Ordered, comparable and any can be availed in the aforementioned package constraints.

tcard commented

Type lists for expressing operator constraints seems inelegant to me.

  • It's an indirect, non-obvious reflection of intent. You really just want to say "this and that operator must be defined for this type". Instead, you're listing some concrete types which happen to indirectly express such constraint.
  • It breaks the distinction between concrete types and their common interface. You talk about interface by talking about its concrete implementors. This shows in how it sort of captures aspects of sum types (which are about concrete types and have nothing to do with generics).
  • It's an artifact of the fact that Go doesn't have operator overloading. If custom types could define how they implement operators, you couldn't just list all types that define some operator. This innecesarily brings a separate, orthogonal design decision of Go into generics.

A better alternative would express operator constraints explicitly and directly. I have only sketches of ideas.

  • In the type parameters list, add an optional section, following a ;, with expressions where type parameters represent values of such types, and the resulting code must typecheck for the assigned concrete types.
[T SomeInterface; T == T, T + T, []T, T <-, <-T, T - T, -T]
  • Pros: Explicit, clear.
  • Cons/doubts:
    • Verbose
    • Can't combine and name them (ie. can't say "comparable")
    • Does []T imply T[:]? Is T[:] on its own supported? Are there other implications like this?
    • Does a expression need to be "canonical"? Can you express the same constraint in several ways?
    • Are conversion expressions supported too? That's again kind of sum types territory.
  • But: You'll rarely need more that one or two such expressions.
  • Constraints can take a list of operators as arguments.
[T SomeInterface(==, +, [])]
  • Pros: Concise,
  • Cons:
    • New, non-obvious expression-like language construct.
    • Unary vs. binary -; send vs. receive <-.
    • It looks like calling, or parameterizing the interface constraint, but it isn't.
  • Bring back contracts, but don't allow methods on them; that's what interface constraints are for.
[T SomeInterface; SomeContract(T)]

Those sketches also fix another aspect I find awkward: interfaces aren't "partially expanded" ("partially" in that expanded ones are disallowed for interface values) as as result of them being the only way to contraint types. But that's besides the case against type lists.

Anyway, this proposal as is would be great to have. Thanks for the hard word.

The design is silent(unless I missed it) on why comparable needs to be predeclared but Ordered doesn't.

To put it simply, Ordered doesn’t need to be builtin because the Ordered types are just a list of types (the real numeric types + strings)

However, the Comparable types are a different story because some structs are comparable while other structs are not, and they can’t be defined by a simple type list like Ordered can be. The only way under this proposal would be with a builtin constraint.

@tcard

It's an artifact of the fact that Go doesn't have operator overloading.

operator overloading is a rare feature among mainstream languages, if we exclude ruby, the only language I know of in widespread use that offers operator overloading is C++.

tcard commented

@davecheney I meant generally the ability for types to provide custom implementations for operators, not the particular C++ flavor of it. I believe that's a common use for the term (for example, Kotlin, Rust, Swift), but I agree it's not the most rigorous one.

@tcard

It's an indirect, non-obvious reflection of intent. You really just want to say "this and that operator must be defined for this type". Instead, you're listing some concrete types which happen to indirectly express such constraint.

I don't think it is that simple. For example, comparisons have different semantics for int and float64 - the former is totally ordered, the latter isn't. + is very different from - (the former works with strings, the latter doesn't). range behaves differently on maps, channels and slices and strings. I've written about this here, when the design was still centered around contracts, which work very similar to your first sketch.

FWIW, I think this falls under "old comments". The decision for type-lists was done after criticism of the design of contracts, for exactly these reasons. As much as I personally agree that type-lists are awkward, I think they are here to stay.

@Merovius

As much as I personally agree that type-lists > are awkward, I think they are here to stay.

Well, afaic I do appreciate the presence of type lists. I think that bounding interfaces extensively(i. e. Listing the set of types implementing it) was needed.

This is another topic entirely, but I would focus more on that aspect. I think that if we could have that as a kind of guards around interface values throughout the language, it would already alleviate some of the pain points that makes people want to reach for full-blown parametric polymorphism. That with perhaps a little zest of hand-written user-space simili- specialization using type-switches.

That would evidently only works if such extensively-defined interfaces only use concrete types. No embedding of traditional interfaces to define type-list interfaces or we lose the bounding.

(As a matter of fact, without meaning any offense, it's the current design that I feel (emphasis on feel, not a fact) a bit awkward. Because it's limiting and has quite a handful of language-changing edge-cases depicted at the end of the design document. Yet I definitely see the worth of the overall picture, especially for datastructure reuse.

tcard commented

@Merovius

FWIW, I think this falls under "old comments". The decision for type-lists was done after criticism of the design of contracts, for exactly these reasons. As much as I personally agree that type-lists are awkward, I think they are here to stay.

Then maybe it should be added to the "Discarded ideas" section of the proposal. It does mention contracts, but not this issue in particular. (Although I'm seeing now that "Why not use methods instead of type lists?" does discuss this issue.)

While I'm here, I'll just add that I see the value of type-lists for statically requiring a closed set of specific types, but I'm not sure the awkwardness is worth it. Even type-heavy languages like Haskell or Rust do allow you to program against the operator's interface, even if concrete behaviors differ.

tmke8 commented

@jorng

That was before the idea of composite type in constraints, though... wasn't it? Now they are different things. One cannot use a constraint with composite types as a traditional interface.

As far as I can tell there is no fundamental reason why using such a type as an interface shouldn't be allowed. And the proposal says as much:

Interface types with type lists may only be used as constraints on type parameters. They may not be used as ordinary interface types. [...]

This restriction may be lifted in future language versions. An interface type with a type list may be useful as a form of sum type, albeit one that can have the value nil.

I understood this as "we could allow this, but it would increase the scope of the proposal too much". So, I don't think this weakens the claim that constraints and interfaces should be the same thing: the proposal even gives an example where a type list in a traditional interface would be useful.

noxer commented

@griesemer

Regarding your comment on any meaning "substitution" or "reference" depending in use: any just means interface{}, nothing more, nothing less. But a type parameter list is instantiated (through substitution) at compile time; an ordinary parameter list is assigned to (via function invocation) at run time.

I'm opposed to this: interface{} is much more clear for beginners (once they've learned about interfaces). If you really want to include this (as it is shorter), would you also be willing to include empty as an alias for struct{}? That would make it (in my eyes) more consistent as those two are "special" types.

Thank you all for working on this proposal.

@robpike's talk "Simplicity is Complicated" really influenced my way of thinking about language features and I feel like this proposal contradicts some of the points of his talk. Concerns about the complexity have already been expressed here, so let me just add this example: I'm convinced that many programmers will start using filter/map/reduce instead of loops, once these functions are available. I dislike this idea mostly because orthogonality would be reduced.

By letting users choose between two constructs, code becomes less predictable. Currently I know for sure that I have to go looking for a loop, when I know a slice is transformed somewhere. If slices.Map were available, I would also have to look for this in addition to a loop. This slows me down while reading code.

I'm also slowed down while writing code, because I have to decide whether a loop or filter/map/reduce is better suited for my code.

@robpike's talk "Simplicity is Complicated" really influenced my way of thinking about language features and I feel like this proposal contradicts some of the points of his talk. Concerns about the complexity have already been expressed here, so let me just add this example: I'm convinced that many programmers will start using filter/map/reduce instead of loops, once these functions are available. I dislike this idea mostly because orthogonality would be reduced.

By letting users choose between two constructs, code becomes less predictable. Currently I know for sure that I have to go looking for a loop, when I know a slice is transformed somewhere. If slices.Map were available, I would also have to look for this in addition to a loop. This slows me down while reading code.

I'm also slowed down while writing code, because I have to decide whether a loop or filter/map/reduce is better suited for my code.

In addition to your points, I'd argue that these sort of constructs hide a lot of time complexity which seems to go against the Go paradigm where expensive operations are generally very explicit. For these reasons perhaps they don't belong in the SDK packages. I could understand this decision to have them live in a x/slices module or similar. Users could then opt in to using this module or not. Personally I think they're useful enough that they should be in the SDK itself though.

Let's not see this as an argument against generics in Go though. Currently the language lacks the semantics to implement these concepts. Surely that's clear evidence that generics are dearly needed.

In the call NewPair(1, 2.5) both arguments are untyped constants, so we move on the second pass. This time we set the first constant to int and the second to float64. We then try to unify F with both int and float64, so unification fails, and we report a compilation error.

The special case of untyped numeric constants could be handled by taking the "largest" default type of the constants, defined as complex is larger than float is larger than int. Then NewPair(1, 2.5) unifies F with largest{int, float64} = float64. That doesn't seem too complicated to specify or implement and it certainly seems to me like it would be more straightforward to use.

@codesoap

By letting users choose between two constructs, code becomes less predictable. Currently I know for sure that I have to go looking for a loop, when I know a slice is transformed somewhere. If slices.Map were available, I would also have to look for this in addition to a loop. This slows me down while reading code.

This seems a bit strange to me. Isn't that true for basically any function that does anything remotely complicated? I get that the concern is amplified because you think people will use them a lot, but, conversely, if people use them a lot doesn't that mean that people will also get used to them being used?

@Tatskaari, that paradigm is generally for language features, not for standard library functions. This proposal adds basically nothing to the runtime, and certainly no hidden functionality. A function can have whatever functionality it wants. It would be really weird to say, for example, that all standard library functions have to be O(1). If generic slice manipulation functions are a problem, then what about bytes? strings? bufio? Your argument reads to me as being that a function 'hides' time complexity because the code is defined in a different package, which seems like a completely unreasonable standard.

@qinabu, that doesn't work as a basis for generic functionality because it doesn't allow you to declare any relationship between multiple parts of the function signature, and also doesn't allow you to handle returns. For example, what about a sum function?

// The return is the interface type Add, not the actual element type.
// There is no way to declare it as being the same as the element type.
func Sum(s []Add) Add

// v is Add, not int.
v := Sum([]int{3, 5, 2})
package p1

// S is a type with a parameterized method Identity.
type S struct{}

// Identity is a simple identity method that works for any type.
func (S) Identity[T any](v T) T { return v }

package p2

// HasIdentity is an interface that matches any type with a
// parameterized Identity method.
type HasIdentity interface {
	Identity[T any](T) T
}

package p3

import "p2"

// CheckIdentity checks the Identity method if it exists.
// Note that although this function calls a parameterized method,
// this function is not itself parameterized.
func CheckIdentity(v interface{}) {
	if vi, ok := v.(p2.HasIdentity); ok {
		if got := vi.Identity[int](0); got != 0 {
			panic(got)
		}
	}
}

package p4

import (
	"p1"
	"p3"
)

// CheckSIdentity passes an S value to CheckIdentity.
func CheckSIdentity() {
	p3.CheckIden

But package p3 does not know anything about the type p1.S. There may be no other call to p1.S.Identity elsewhere in the program. We need to instantiate p1.S.Identity[int] somewhere, but how?

The natural way would be to fetch instance sets (sets containing method implementations for any specified interface, here HasIdentity) as a matter of reflection.

However, solving instance resolution over reflection alludes to the following questions:

  • Does reflection support generic instantiation?
  • Are generic types/functions are part of the runtime type information selectable for runtime reflection?
  • Are type to interface conformance relationships are part of the runtime type information?

If all these questions can be answered by yes, then the case highlighted above is solvable.

Alternatively, modulating the "CheckIdentity" function by the compiler to:

func CheckIdentity(v interface{}, hiddenHasIdentityInstanceContainingPointerToIdentityMethod) {
	if vi, ok := v.(p2.HasIdentity); ok {
		if got := vi.Identity[int](0); got != 0 {
			panic(got)
		}
	}
}

serves providing an instance automatically by calling "CheckIdentity" function. This hidden parameter needs to be up-propagated to all callers passing p1.S down the caller-callee hierarchy.

Adding type list is great idea, but I think type matching rules are a bit confusing and too implicit.
There is a interesting proposal to add explicit syntax like

type AnyInt interface {
    type interface int, interface uint
}

@sighoya

  • Does reflection support generic instantiation?

See http://golang.org/design/go2draft-type-parameters#reflection.

  • Are generic types/functions are part of the runtime type information selectable for runtime reflection?
  • Are type to interface conformance relationships are part of the runtime type information?

See http://golang.org/design/go2draft-type-parameters#generic-types-as-type-switch-cases and http://golang.org/design/go2draft-type-parameters#operations-permitted-for-any-type.

@tdakkota I think that syntax a) is terribly redundant - the interface does not add anything to parseability (for machines or humans). And b) terribly confusing. The matching rules have nothing at all to do with interfaces, so suggesting that via syntax seems like a bad idea. And c) doesn't actually add anything, because the matching rules (which are supposedly a problem because they are confusing) stay the same. Type-lists are already a new syntax to learn, so I don't think there's an issue with having to learn the matching rules in the same go.

@Merovius I don't agree that it

is terribly redundant

and

doesn't actually add anything

type interface int64

means that type can be anything which have int64 interface, it matches int64, time.Duration or any other new type of int64

type int64

means that type can be only int64.
That differs from current go2go matching rules. This syntax is more explicit.

If you think that this syntax should not use keyword interface, so I agree. But adding a new keyword can cause compatibility problems.
There are some additional comments in thread, for example @bcmills proposed to use underlying instead of interface.

type interface int64

means that type can be anything which have int64 interface, it matches int64, time.Duration or any other new type of int64

type int64

means that type can be only int64.
That differs from current go2go matching rules. This syntax is more explicit.

Not too bad. It may be a bit annoying for a new gopher to understand because of the overloading of the word interface. But as I think that matching underlying should only be opt-in at best(for proper bounding) , I think it brings value to have specific notation for it. [edit] prolly needs a specific notation because it only works for built-in types as you've defined but the idea has merits.

I like it.

I guess I'd really rather have nice type-lists / bounded interfaces pervasively before "generics". I remember in the old days trying to implement a Concurrent Hash-Array Mapped Trie in Go and the nodes being algebraic datatypes were a bit annoying to do wrt data locality/bounding.

A trie of user-defined structs would still be annoying to implement without parametric polymorphism but is it really needed?

fzipp commented
type interface int64

means that type can be anything which have int64 interface, it matches int64, time.Duration or any other new type of int64

type int64

means that type can be only int64.
That differs from current go2go matching rules. This syntax is more explicit.

If you think that this syntax should not use keyword interface, so I agree. But adding a new keyword can cause compatibility problems.

I'm not sure if such a distinction is really necessary or if I like it. But what about:

type int(_), int32(_), int64(_), float64, float32

3x underlying type, 2x exact type. The (_) is meant to symbolise a type conversion.

It would probably be better to discuss that in the existing issue.

Well, it affects the design of type lists. I'd argue it can stay here.