proposal: spec: add generic programming using type parameters #43651
Comments
This comment has been hidden.
This comment has been hidden.
This comment has been hidden.
This comment has been hidden.
This comment has been hidden.
This comment has been hidden.
This comment has been hidden.
This comment has been hidden.
an I'm also having trouble thinking how you could implement any |
This comment has been hidden.
This comment has been hidden.
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:
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:
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 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". |
Valid point, in your example, an interface would be the better choice. However, a more apt use case of generics would be |
What are the plans for amending the standard library to utilize generics? I see two important tracts of work here.
|
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 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 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. |
The new "any" is not different from interface{} as a type constraint. |
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 |
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'). |
I'd expect a linter warning: "useless use of type parameter" |
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. |
This comment has been hidden.
This comment has been hidden.
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 Interfaces are a strange exception to this. Despite the fact that a type is defined, as in |
This comment has been hidden.
This comment has been hidden.
This comment has been hidden.
This comment has been hidden.
@p-kraszewski The most up-to-date development is happening on the |
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. |
This comment has been hidden.
This comment has been hidden.
This comment has been hidden.
This comment has been hidden.
govet or golint could help with that. |
@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 |
I'm concerned about the examples under the "Constraint type inference" section. Let's take the In principle this should be a simple task, it's a function that wants to talk about a type T and the methods defined on its pointer type *T. Methods with a pointer receiver are basically universal in Go so this is something that will probably come up often. The soulution, however, involves a number of new concepts: parametric constraints, type lists inside interfaces, using a type parameter in a type list, non-straightfoward type unification... Also the section on complexity says:
but I don't think this is true for this specific case (to be honest I'm not sure it can ever be true in the way it's written, but especially in this case). If I come across a function:
and wish to call it, how many types do I have to specify? Is it two, A and C, with C being somehow related to A (for example C is a function type with an argument of type A), or is it just one type, A, and C only exists to talk about an additional constraint on a derived type of A? I think reading generic code would be easier if one could write FromStrings like this:
reading this it's clear that we are just talking about a type T and the second parameter is just there to specify additional constraints on a derived type. Syntactically this means allowing a type literal, not just an identifier, to appear as the left-hand side of a type parameter. Semantically when the type parameter A similar bit of syntactic sugar would help with the other example. If non-interface types could appear as constraints DoubleDefined could be written as:
Any time a non-interface constraint appears it would be equivalent to |
This comment was marked as off-topic.
This comment was marked as off-topic.
This comment was marked as off-topic.
This comment was marked as off-topic.
@aarzilli We used to have an idea similar to what you suggest, but many people found it deeply confusing. A problem with it is that That said I agree that constraint type inference is one of the more complex aspects of the proposal, and it would be nice if we could simplify them somehow. But we don't want to confuse additional constraints with type parameters. |
Well, that's a shame. I hope you explore other syntactic options for this. The current solution is very elegant, handling this cases as a side effect of the interaction between type lists and type unification. However, in my (limited) experience, people who are not already familiar with complex type systems find parametric constraints and type unification alien and difficult to understand. You would not see this effect if the cohort who has tried generic go so far consists of people interested in PL design, because they are all already familiar with that. What about the second part of my message, about allowing |
From a marketing perspective this should better be called Go 2. What other planned change may ever better justify this step? |
@martinrode From a marketing perspective, calling anything Go 2 also has downsides, in that it will lead people to make the same mistake @bobg did - assuming that it is a point breaking compatibility and that they have to choose a Go version to use/support, or anything. I don't really care if anything is called Go 2 or not, FWIW. But I feel like even talking about "Go 2" has strange effects on people's assumptions of how Go development and progression works. |
I understand your point, but I still disagree. Since this is going to be the biggest change to the language sine 1.0, I argue that it must be called 2.0. Many other big languages like Java or C++ regularly bump their major version numbers without breaking changes (I am not looking at you Python), so I say that is the default and its what people expect. So no worries let‘s do this now or never. Hiding such a great feature behind a one percent increase would be a shame. |
Sorry for not mentioning that. I think what you are suggesting is a kind of syntactic sugar for a structural constraint. We permit an arbitrary type C to be used as a type constraint. When Edited to add: to clarify, the paragraph above is not a description of the current proposal. It is my restatement of my understanding of what @aarzilli was suggesting in #43651 (comment). |
In the Go ecosystem, particularly Go Modules, |
I was curious how much of a limitation not having methods would be so I started experimenting. I think you can simulate at least some of the behaviors without generic methods. For instance, imagine you wanted some generic type Scanner interface {
Scan(dest ...interface{}) error
}
type DB[T Scanner] interface {
QueryRow(query string, args ...interface{}) T
} This can now be implemented with both type MockScanner struct {
ScanFn func(dest ...interface{}) error
}
func (ms *MockScanner) Scan(dest ...interface{}) error {
return ms.ScanFn(dest...)
} Now if you want to use this type inside something like a type WidgetService[T Scanner] struct {
DB DB[T]
}
// This won't work because we can't create generic methods.
func (ws *WidgetService) Create(w *Widget) error {
err := ws.DB.QueryRow(`
INSERT INTO widgets (size, color)
VALUES ($1, $2)
RETURNING id`, w.Size, w.Color).Scan(&w.ID)
if err != nil {
return fmt.Errorf("create widget: %w", err)
}
return nil
} One alternative is to write a func create[T Scanner](db DB[T], w *Widget) error {
err := db.QueryRow(`
INSERT INTO widgets (size, color)
VALUES ($1, $2)
RETURNING id`, w.Size, w.Color).Scan(&w.ID)
if err != nil {
return fmt.Errorf("create widget: %w", err)
}
return nil
} But, that means our type Server struct {
WidgetService interface {
Create(w *Widget) error
}
} We can't use our generics type with that in any easy way. One alternative is to use a struct with function fields. // Still testable/mockable my swapping out each function field.
type WidgetService struct {
Create func(*Widget) error
}
type Server struct {
WidgetService WidgetService
} Then you can simplify creation with some code like: func NewWidgetService[T Scanner](db DB[T]) WidgetService {
ws := WidgetService{}
ws.Create = func(w *Widget) error {
// create() here is the same create[T Scanner].. from above
return create(db, w)
}
return ws
} I have no idea what the implications of this is in terms of code readability, maintenance, etc, but at first glance I think I would still be able to write a lot of the code I currently write using an approach like this. It might not feel as elegant, but it should work. Here is a playground link: https://go2goplay.golang.org/p/bluhM6TCG0R What am I missing? I'm sure there is some edge case I'm not seeing. PS - I realize the specific example I used with |
@joncalhoun I'm having trouble understanding your example. In particular, where you write Generally, I'm skeptical that function-values as a struct field can address the use-cases prevented by the limitations on extra type-parameters for functions. That's because the restriction is in place for relatively fundamental implementation issues - and semantically, an interface value can also be viewed as a struct with function value fields. So if they could address the issues, we could just transfer that solution to methods and interface-satisfaction. To be clear, I think to actually address the issue, you'd have to be able to transform something like this: type X struct {
}
// Extra type parameter T, not mentioned in the declaration of X.
func (x X) F[T any](T) {
} Into this: type X struct {
// Extra type parameter T, not mentioned in the declaration of X.
F func[T any](T)
} And this can't work - it would require you to get a function value of an uninstantiated generic function. This is prohibited by the design - for very similar reasons as why methods with extra type-parameters are prohibited. |
@Merovius Is the Go2Go playground not up-to-date then? I couldn't find the correct way to add a method to a generic type. See https://go2goplay.golang.org/p/1vIb794CaBm for a broken example I made while experimenting. |
@joncalhoun You have to mention the type-parameter in the receiver: https://go2goplay.golang.org/p/ZV873kDkeEf |
Huh.. I swear I tried that unsuccessfully. Thanks! I misunderstood then - I was thinking methods on generic types weren't part of the proposal and was using closures to sorta hack around it. I'd have to look at the case you are describing more closely, and about to jump in a zoom. Will look later though. |
I don't see that behavior described in the current draft of the design (perhaps I've missed it?), but if I understand that statement correctly I think that definition is not coherent with the rest of the design. Both in the Featherweight Go paper and in the published Type Parameters draft design, a type constraint is an upper bound on the types that can be passed for the type argument: the actual argument is always a subtype of the constraint type, in the sense that an expression of the argument type can be used in any context that permits an expression of the constraint type. (As it is phrased in the draft design: “Calling a generic function with a type argument is similar to assigning to a variable of interface type: the type argument must implement the constraints of the type parameter.”) However, the type Consider the types |
It's not in the current proposal. I was restating @aarzilli 's suggestion in my own words. |
I could not find any discussion of structural typing, or "how do I ask for any struct (or struct pointer) with a field named X of type T?", in the proposal. Feel free to link me against it if that took place somewhere else. I see that if you define a type constraint with structs that all have the same field name and type, that you can use it, but that requires enumerating all the possible input types, something that may not be feasible. I think some cases could be solved by getters, but this both adds complexity and may not be feasible for all cases. |
@bcmills note that you can do it anyway, https://go2goplay.golang.org/p/i_5dyMuTaCk |
@carnott-snap You are correct: the proposal does not provide a way to write a constraint for "any struct with field X of type T". I think it might be possible to add such a constraint in the future. I don't know if that would ever actually happen, though. It seems like a special case. The contracts design draft did support it in a way, but that was more or less by accident, not because it seemed important. |
Having just re-read most of the design, the one thing I strongly object to is the use of structural constraints to allow omitting some of the type parameters at the instantiation site, as in the Pointer Method Example. I believe that instantiation should require all type parameters to be specified unless they can all be inferred. This will improve clarity, since the reader never has to wonder if he's looking at a partial set of type arguments. It also encourages API designers to aim for full inference since there's no halfway point. With what I'm suggesting, the "Element Constraint example" would work, but not the "Pointer Method Example". If the latter turns out to be a common scenario and true pain point, the restriction could be relaxed in later versions. |
As long as there is not fundamental opposition to the concept, that is fine. I do not have a solid use case for the need, but it sounds like if one came up, we could work to a resolution. The biggest abstract concern I have is that lack of this feature will push people to use getters over fields. Currently the inverse is cannon, and we may not want to see that pattern change, or we may be fine with it, but we should be clear about the implications to style. |
@OneOfOne Your example is not what @bcmills is talking about:
In your example, you cast the value to The use of generics does not change this behavior. |
Since |
Nothing+optionalsemicolon would be more succinct to write than |
@atomsymbol But permitting type parameters to not specify a constraint would be ambiguous, as discussed previously. |
we already have similar concepts like |
@griesemer
But that's actually not true, because even interface types have identity.
It's important that |
Various people have suggested using a type-switch on an interface value to determine the actual type of a type parameter with code like this:
I'll just note that this code won't work reliably for all possible types, because if For example: https://go2goplay.golang.org/p/V5aMUc4MoGc Using a pointer to the type makes it reliable (although it's still not possible to switch on underlying types, of course): https://go2goplay.golang.org/p/b6V6xkFYmGB Even when you've done such a type switch, you'll still need to use dynamic (and therefore error-prone) type conversion to access values of parameters and assign to return parameters. For example:
Personally, one significant issue I have with the proposal is the way that the type matching on underlying types makes type switches on generic type parameters untenable. If there was no matching on underlying types, then a type switch on type parameters could be usefully used to implement efficient and type-safe ad hoc specialisation (for example to use efficient machine code implementations for specific known types). It occurs to me that one possibility might be to allow type switches on type parameters, but only when those parameters aren't type-list interfaces (i.e. when there's no possibility of ambiguity between actual type and underlying type). |
|
We propose adding support for type parameters to Go. This will change the Go language to support a form of generic programming.
A detailed design draft has already been published, with input from many members of the Go community. We are now taking the next step and proposing that this design draft become a part of the language.
A very high level overview of the proposed changes:
func F[T any](p T) { ... }
.type MySlice[T any] []T
.func F[T Constraint](p T) { ... }
.any
is a type constraint that permits any type.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.
The text was updated successfully, but these errors were encountered: