New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
proposal: Go 2: generic types (GT) #39669
Comments
It seems very subtle to change a function from a normal function to a parameterized function based on whether the parameter type is an interface that says In this approach is there a way to instantiate a function without calling it? |
Generic types will likely be fairly special and widely recognized as such?
Edit: ignore this -- better idea just below, now added to main proposal:
func StringerFunc(s []Stringer) (ret []string) { ... }
...
s := []MyStringer{"a", "b"}
sf := &StringerFunc(s)
|
I guess I don't see any particular reason to think that type constraints will be clearly different from interface types. Some will, some won't. To me |
Actually a better idea is to just use type expressions for the args -- not an actual variables -- that would clearly disambiguate and is more semantically appropriate -- basically just like you'd do with the type parameters: func StringerFunc(s []Stringer) (ret []string) { ... }
...
sf := StringerFunc([]MyStringer) // type-only args = instantiated version Edit: just added this to proposal |
I'm not sure I understand your point: I'm just saying that anything in the |
I said that it seems fairly subtle to decide whether a function is generic or not based on the characteristics of the parameter types. You said it would be clear if the type comes from the constraints package. I agree, but I said that in general I don't see any particular reason to think that type constraints will be clearly different from interface types. If they come from the constraints package, it will be clear. If they don't, it won't. |
Ok, right. well, that was my other claim that the generic types would be relatively rare and well known. I've seen discussion with this assumption before, e.g., in saying that not many people would be writing constraints in the first place (back when they were contracts), so it wasn't so important that it be easy to write them... But anyway you're obviously correct in pointing out this important limitation for the GT proposal: if people don't generally know that they're dealing with a generic type, and understand the implications of that, it could be confusing. Some further ideas:
|
Also, from go-nuts just now, you said:
In which case, the function signature would contain the generic |
I commented on wrong issue, moved here: #39684 |
Why is the syntax |
how would you declare the equivalent of: func F(type T)(slice []T, elem.T) {} like this? func F(slice []T, elem type(slice)) {} |
@seankhliao I assume you meant this (no func F(type T)(slice []T, elem T) {} and yes that is how it would work, although func F(slice []type, elem type(slice)) {} |
@urandom I'm not sure exactly what you're asking -- in general However, there is one exception which might be what you're asking about. To support the ability to pass type parameters to functions like |
Would this proposal allow currying for generic functions? e.g. func F(type T, slice []T) {} type A struct {}
fa := F(A)
fa([]A) // allowing this |
The draft proposal specifically omits currying, and it doesn't seem to be common practice, or particularly elegant, in Go. And in your example, the first arg is just a type parameter, not a real parameter that the function uses, so I think that would be called defining an instance of the function with a concrete type parameter, not currying per se. In any case, the first arg would be omitted under this proposal, yielding: func F(slice []type) {}
type A struct {}
fa := F([]A) // this is defining an instance of the function with a concrete type
sa :=make([]A, 2) // or whatever
fa(sa) //works fine if you wanted to do Go style currying, this is what it would look like I guess: func F(a type, s []type(a)) {}
type A struct {}
// curried function -- return function arg type explicitly defined in terms of arg type of a
func C(a type) func([]type(a)) {
return func (s []type(a)) {
return F(a, s)
}
}
AV := A{}
fc := C(A) // given the explicit connection between 'a' type and 's' type,
// compiler COULD instantiate everything, such that function C can be fully concretely complied..
sa :=make([]A, 2) // or whatever
fc(sa) // if arg is not []A, it is an err given instantiated type of fc This is sufficiently complex that maybe it wouldn't make sense to support such a thing. And if one or more of the arg types remains undefined when the curry function is instantiated, then that wouldn't work I think.. The more typical way this works in Go is to use methods with a "curried receiver", which is very convenient for "callback" functions to pass additional state etc. |
In this proposal, would |
That would be easier to "type" :) Some similar discussion on go-nuts about |
Could an // Package orderedmap provides an ordered map, implemented as a binary tree.
package orderedmap
import "chans"
// Map is an ordered map.
// note: presence of type args defines a generic struct.
// if wrote: `K, V any` then K and V would be constrained to be the *same* generic type.
// could have written: `K number, V any` etc to specify constraints.
type Map(K any, V any) struct {
root *node(K, V)
compare func(K, K) int
}
// node is the type of a node in the binary tree.
type node(K any, V any) struct {
k K
v V
left, right *node(K, V)
}
// New returns a new map.
// note: first two args are type args (type comes first), not generic var args
func New(type K, type V, compare func(K, K) int) *Map(K, V) {
return &Map(K, V){compare: compare}
} |
@rcoreilly the OP specifies two arguments must have the same type if specified as If type arguments can be omitted if the type can be inferred, what do you think of this alternative? // s1 and s2 can be slice of any type, but it must be the *same* type
func Print2Same(type T, s1, s2 []T) { ... }
// Functionally identical to above
func Print2Same(type T, s1 []T, s2 []T) { ... } |
maybe, but OTOH, that expression can only be used with concrete types when the types are the same, so the semantics are the same... And for your alternative, I think you're using the draft proposal logic, not this GT proposal, which doesn't use the type arg at all, and would look like this: // s1 and s2 can be slice of any type, but it must be the *same* type
func Print2Same(s1, s2 []type) { ... }
// Functionally identical to above
func Print2Same(s1 []type, s2 []type(s1)) { ... } So the shared type syntax is definitely a bit simpler. |
I think your proposal, overall, is superior to the current draft proposal for Go parametric types. My suggestion was more to do with what I think is the weakest part—declaring that two args must be of the same type by omission of the type from all but the last argument. This seems to make more sense, and is explicit, particularly to the reader: func Print2Same(s1 []type, s2 []type(s1)) { ... } It’d also work if the arguments that must be the same type aren’t sequential: func f(a type, f func(type(a)), b type(a)) { … } |
Somewhat related: func f(a any, f func(type(a)), b type(a)) { … } |
@ydnar I agree about the benefits of using the explicit type expression and thanks for the support! And I agree also that |
Would your proposal work with |
We're going to move forward with the current generics design draft, at least for now (https://go.googlesource.com/proposal/+/refs/heads/master/design/go2draft-type-parameters.md). One of the cited reasons for this proposal was the extra parens, which have been removed from that draft. This is a likely decline. We can revisit that decision if the current design draft is not adopted. Leaving open for four weeks for final comments. |
Yep the square brackets seem like a good improvement. Ultimately there is X amount of additional complexity associated with generic types, and either you explicitly list them as arguments, as in the design draft and most (all?) other implementations in other languages, or you have to use the various type expressions like those in this proposal, and there are trade-offs either way. The advantages of using the established approach (so people coming from other languages will find it easy to understand) and explicitness of naming the type args are clear, so overall it probably makes sense to pursue the current approach. @ianlancetaylor thanks for all your effort and patience in engaging with this and so many other issues! |
No change in consensus. |
So this doesn't mean that Generics won't be implemented, just that the existing Proposal will be used in the implementation. Correct?
|
It means that we are going to turn the existing design draft (https://go.googlesource.com/proposal/+/refs/heads/master/design/go2draft-type-parameters.md) into a proposal, see if that gets approved, and, if it does, implement that. |
This proposal changes the syntax for type parameters on functions, to eliminate the extra parens which are widely raised as a problem with the draft proposal. It retains type parameters for types, and should otherwise be very similar to the draft proposal, just with a simpler syntax.
The key idea is: use generic type names just as we use concrete types now, instead of having separate type parameters. (Generic Types, GT)
GT puts all the type information in one place, instead of distributing it across two locations, greatly reducing cognitive load, and eliminates the extra parens in function calls which are confusing and a constant complaint about the draft proposal.
interface
types, so use these interface types directly as type names.type
.interface
types: generic, and non-generic.type type
.Compare the following examples against those in the draft proposal -- starting at the start:
For constrained types:
Compare this to:
To emphasize the difference from the draft proposal, here's a direct comparison:
GT consolidates the type information in one place, where it has always been.
Type expressions
To refer to the concrete type of a generic arg, use
type(x)
-- this is needed for return values etc:For slices, maps and channels,
type(x)
returns the element type -- for maps,type(m[])
returns the key type:For
func
types with generic arg / rval types, you must name any args or return values you need the type of, and access like a named field:type(f.arg2)
.This is the main downside of the GT proposal -- referring to the type elsewhere is now more cumbersome. Fortunately, Go's type inference means that this doesn't happen that much. And if a given arg type is going to be used a lot, you can define an inline type alias as is done in the draft proposal (see Container example below).
Generic types
Generic types are essentially identical to the draft proposal (except with the different type naming convention).
Additional proposal (emailed to go-nuts):
m.T
)type(m.T)
to refer to the type andm.T
to refer to the field (can define a type alias fof the type expression if used frequently).Methods may
nottake additional type argumentsFrom the draft proposal:
There would seem to be no reason to have such a constraint under GT, as generic args are really no different syntactically than concrete ones -- no extra parens, etc.
Type Lists in Constraints
Type lists are exactly as in the draft proposal, and their presence is essential for making the type a generic interface type (
type type
being the fully unconstrained version of this).Type args
To enable
New
functions, and any other case where type values need to be specified as such, we need to support explicit type arguments -- these are just like regular arguments, in the same parenthesized list, but start with thetype
keyword and must be passed a type expression (a type literal or atype()
expression).Instantiated generic function
Edit: based on comments below:
To refer to a concrete instantiated version of a generic function, specify the args as types -- that clearly differentiates from actually calling the function, and looks like the equivalent with explicit type args:
Containers Example
This provides a good example of how it all works -- very similar to the draft example overall because parameterized types are essentially the same, so it doesn't really show off the main strengths of the GT proposal, but at least concretely demonstrates that it should have the same overall expressive scope.
Note: this GT proposal builds on the core idea from the Generic Native Types (GNT) proposal, but is much simpler and closer to the draft proposal.
The text was updated successfully, but these errors were encountered: