how to update APIs for generics #48287
Replies: 45 comments 211 replies
-
Would definitely be confusing. Especially if you converted from the former to the latter while debugging something. |
Beta Was this translation helpful? Give feedback.
-
Personally, I don't like the asymmetry this would create between type-parameters and regular parameters. Default values for arguments is something that has often been asked about, but we never did it. To me, the case for default type arguments isn't really stronger than for default regular arguments though. I also don't like introducing a new language feature just for a one-time migration. If the motivator for this feature is just the migration of pre-generics APIs to post-generics APIs, it will become de-facto obsolete in a year or so. I don't like that idea. |
Beta Was this translation helpful? Give feedback.
-
One more data-point: This strategy wouldn't help with |
Beta Was this translation helpful? Give feedback.
-
Using new major versions for stdlib packages, e.g. |
Beta Was this translation helpful? Give feedback.
-
Another approach is to observe that the language supports type aliases to support transitions. So let's use type aliases. type Pool[T any] ...
type Pool = Pool[interface{}] The rules here would be:
This alias rule does not work for functions. We can either say there is no transition for functions, or we can introduce another rule. A function may be defined both with and without type parameters. A reference to the function (not a call) with no type arguments is permitted, and gets the version without type parameters. A call of the function with no type arguments gets the version with type parameters and does type inference as usual. If type inference fails, the call is instead made to the version without type parameters (and may fail if the arguments type are not assignable). That gives us func Min[T constraints.Ordered](a, b T) T { ... }
func Min(a, b float64) float64 { return Min[float64](a, b) } |
Beta Was this translation helpful? Give feedback.
-
It's clearly essential to always have a crazy idea that everybody can reject, so here is one. default Pool Pool[interface{}]
default Min Min[float64] |
Beta Was this translation helpful? Give feedback.
-
I'm going to go out on a limb and suggest that we use the Constructor functions for generic containers get the
I think If we do the above, what are the remaining problematic names? |
Beta Was this translation helpful? Give feedback.
-
The idea clearly exists already, as it was mentioned in this thread but I think it's worth its own suggestion as I think it aligns pretty nicely with what we're asking package maintainers to do in general: Don't introduce generics in most of these libraries. /v2 them:
The normal packages should be implementable in terms of the generic version, so if it doesn't compromise performance we could do the |
Beta Was this translation helpful? Give feedback.
-
I support this suggestion, particularly as I see it as having the potential to be generally useful into the future given a minor tweak to the rules. I'd question the need for the parentheses. I understand that they're there to suggest the "maybe" aspect of the default but I don't think they pull their weight, and there's considerable precedent from other languages (e.g. Rust's default type parameters, Python's default function parameters) that they could be omitted without much confusion. That is, I think it would be fine if the examples were written thus:
|
Beta Was this translation helpful? Give feedback.
-
Is this something that could be handled by the It seems like overkill for something like this, and if a clean alternative can be found than that's probably better, but I hadn't seen it discussed anywhere and introducing incompatibilities was one of the primary motivations for adding the directive, if I remember correctly. |
Beta Was this translation helpful? Give feedback.
-
A proposal/idea related to various more or less vague "go fix" approaches already mentioned around here and there, but specific enough and subtly different enough to warrant separate mention, given this is clearly a brainstorming discussion:
(obviously, the "go2/" and "go1compat/" prefixes being subject to bikeshedding, and go1.18 subject to change to newer) edit: Notably, AFAIU this is also basically how Rust editions are working, so Rust community leaders could be consulted about any nonobvious pros or cons of this approach. FWIW, from a distance it seems to be working for them well enough that they repeated this "phase transition" a few times already. Also, if adopted, this could possibly also help with other bigger "Go2" changes if needed at some point. edit 2: This also seems to me fairly similar to the |
Beta Was this translation helpful? Give feedback.
-
We could teach As an example:
Benefits:
|
Beta Was this translation helpful? Give feedback.
-
Is the plan to keep the generics behaving the same as the original? As implemented as a generic
|
Beta Was this translation helpful? Give feedback.
-
A possible awful idea for rejection: implementation-specific suffixes akin to This builds on the observation:
|
Beta Was this translation helpful? Give feedback.
-
I wonder how much compilation time it would take if the function signatures would also be distinguished by their parameter lists. Preferring a generic implementation over the interface one. |
Beta Was this translation helpful? Give feedback.
-
I personally like the default type parameters approach because:
However, there are down sides (I can think of):
The second favor solution of mine is the
And the cons:
My opinion is, Go should have a clear "Sink and Lift" strategy that "sinks" packages into a "Compatibility group" if the package cannot fully utilize the new language features (similar to the The Sink and Lift operation should be performed alongside a language upgrade. For example, during Go1 -> Go2 upgrade, When user decides to upgrade their code to Go2, they can use Here is the idea I don't like: Prefix/Suffix idea ("Of",
So, to sum up again: my small little brain says it loves the default type parameters idea (maybe just not in that exact syntax). And if we can't have that, then the Just my two brain cells. |
Beta Was this translation helpful? Give feedback.
-
There are many good ideas. However, all are workarounds in some or the other way. Hence, why not be super clear about the switch to a generics based standard library, but in a transparent way. Here is just a rough sketch of the idea:
As long as the API semantics don't change, this should work. Maybe some name mangling is needed to separate both code bases on the binary level. Instead of doing anything for backward compatibility, we are doing something for forward migration. |
Beta Was this translation helpful? Give feedback.
-
Edit: As long as you can add parameters in an alias to a different name ( For example the mathgl packages include 3D math types used by OpenGL bindings / games etc. There are 32 and 64 bit versions of each type, If Go ever permits array size type parameters (#44253), the type alias mechanism could also be used to aid migration in the same way (Vector3 could become an alias for Vector[3]). Another example of math code (mathf.go) in the wild uses the f suffix (sinf, cosf) for 32 bit versions of math functions - type aliases would work there while default parameters wouldn't. For those suggesting library authors break API compatibility, then all packages using those libraries re-writing to use the new API - I think we need to be realistic that this will not rapidly or completely happen. Many tasks currently work fine without generics (for example, 64-bit math code), so many applications and libraries would need to change a large number of files for zero or marginal benefits (changes that may include forking dependencies that are no longer actively maintained). Even new code might continue to use "math" instead of "math/v2", if you are using 64-bit math, the former is shorter, and what's the difference? I think the likely outcome is that users would need to be familiar with multiple ways to do the same thing (across many libraries) for the foreseeable future. Creating such a situation in the name of simplicity seems like a false economy. |
Beta Was this translation helpful? Give feedback.
-
How about the following rule (inspired by Java) for types: if a type instantiation doesn't contain type parameters, then the type parameters default to the constraint types. For example:
Pros:
Cons:
|
Beta Was this translation helpful? Give feedback.
-
Metapoint: I don't think this issue worked very well with the GitHub discussions format. Discussions seem to work better when there is one big proposal with many small details to work out (e.g. there is going to be a slices package, what should go in it?), and then the discussions each burrow in on a detail (e.g., let's figure out a name for slices.Compact). At this point, this issue is still too up in the air (change generic type aliases? add v2 packages? something else?) to drill down on specifics, so each subdiscussion tends to repeat one or two of the main themes with minor variations in the specifics, which makes it very hard to follow. |
Beta Was this translation helpful? Give feedback.
-
I may be “under-thinking” the problem, it feels like too much work on the language to come up with what feels like a workaround. The use cases I can think of feel like they are handled with existing constructs. This is my use case summary, what am I missing?: |
Beta Was this translation helpful? Give feedback.
-
how about suffix 'X'? |
Beta Was this translation helpful? Give feedback.
-
At the risk of adding one more proposal to this already lengthy discussion:
This approach is intended to keep the standard library as similar as possible to what public packages need to do, and step 3 is the only new piece required for this to work. The warts of this approach are also warts any open-source library author will face, and the (optional) additional changes below will help with community migrations as well. Each of these would need to go through its own proposal process.
|
Beta Was this translation helpful? Give feedback.
-
I suspect it is far too late to introduce this comment, but it may be useful to record it for later when we look back at this. Long and short: the reason we are talking about default types here is that we aren't doing type inference. In languages where type inference is adopted, none of the issues being raised here exist because the type parameters are auto-populated by inference. This can definitely be done in a go-like language, as we showed in BitC. I definitely wouldn't adopt everything we tried - if only because the result wouldn't be go. But the heart of the problem here is that type variables are getting instantiated explicitly. Circling back to an early example in this thread:
It appears to me that this is a type error, because the first invocation of
If the promotion is required to be explicit, there is neither ambiguity (to the compiler) nor surprise (to the user). We went to significant lengths in BitC to use type classes to arrange for this type of implicit promotion on binary operators. At the time, I was pushing the type classes idea to see what kinds of things could be expressed with them. We ended up with something similar to go's untyped constants, but we got there using type classes. The problem, ultimately, is that type class constraints accumulate in much the way that explicitly declared exception types accumulate: incomprehensibly. Jonathan Shapiro (the BitC architect). |
Beta Was this translation helpful? Give feedback.
-
Thinking further about constraint accumulation, it will be interesting to see how type constraints evolve in Go. At some point I suspect the language will need to consider intersection and union types to deal with them comprehensively. |
Beta Was this translation helpful? Give feedback.
-
If we had a Go equivalent of the absl::StatusOr, let's call it func ParseNumber[T constraints.Number](s string, base int) ErrorOr[T] The typical
But only states 1 and 2 are sensible in most cases. The Another advantage of func WithFunc[T any](myfunc func()T) T
WithFunc(func()int { ... })
WithFunc(func()ErrorOr[int] { ... }) instead of having 2 versions of func WithFunc[T any](myfunc func()T) T
func WithFuncErr[T any](myfunc func()(T, error)) (T, error) This would make it easier to write generic libraries and APIs. |
Beta Was this translation helpful? Give feedback.
-
@jabolopes Your part of this discussion has drifted far from the original intent of this issue and is now taking up about half the space. Can you please move the discussion to a separate issue and do so in the form of a proposal? This proposal is originally about how to introduce generic versions of things like Min and Map. You are asking for a new way to think about errors. That deserves to be a separate conversation. |
Beta Was this translation helpful? Give feedback.
-
I was recently discussing this on twitter, but instead of suggesting to library designers to provide two versions of each function, one with a predefined constraint, and one with a func value as a parameter -- why not provide functions for those operators in the constraints package? That way, instead of choosing between Sort(v, constraints.LessThan[T]) |
Beta Was this translation helpful? Give feedback.
-
@rsc:
I don't think we'd want to make math.Abs, Min, or Max generic anyway, since they take into account NaN, infinities, and signed zeroes. Are there any other examples of wanting to abstract a non-interface type to a type variable in the stdlib? I think we should follow Java's approach, with raw types. The Go equivalent would be:
So We would get:
Due to the basic interface constraint limitation, we wouldn't be able to convert, say, func Max(x, y int64) int64 {
if x > y {
return x
}
return y
}
var max func(int64, int64) int64 = Max to func Max[T constraints.Ordered](x, y T) T {
if x > y {
return x
}
return y
}
var max func(?, ?) ? = Max because there's nothing we can put for For the cases where we'd need to add a second, generic, variant of a function, I suggest appending the constraint to the name, rather than "Of". E.g. |
Beta Was this translation helpful? Give feedback.
-
I've been holding off commenting here, because from the discussion it doesn't sound like this is a very popular choice, but since nobody mentioned it this way yet and this issue has resurfaced, I figured I would. One obvious thing for me to consider when it's about a single or a few symbols that needs a new version is to version those symbols. That's how I've been solving the Setup function in one of my packages, without generics being involved. I renamed it to SetupV2, changing the signature, then made a new Setup with the old signature calling the new version, also indicating that's what it does in the documentation. I think that's concise and delivers the message, while still being backwards compatible. It's possible to extend it slightly based on some comments in this thread. For example, one could make sure to also create a SetupV1 when there need for SetupV2 arise. SetupV1 and the old Setup would be identical, both calling SetupV2. One could then start thinking about go.mod maneuvering to change the signature of Setup to that of SetupV2 instead of SetupV1 based on the version number in there, but it would be a later problem. Or perhaps to drop the versionless one altogether. |
Beta Was this translation helpful? Give feedback.
-
@ianlancetaylor, @griesemer, and I are wondering about different possible plans for updating APIs that would have used generics but currently use interface{}, and we wanted to cast a wider net for ideas.
There exist types and functions that clearly would use generics if we wrote them today. For example:
There are certainly more of these, both in Go repos and everyone else's code.
The question is what should be the established convention for updating them.
One suggestion is to adopt an "Of" suffix, as in PoolOf[T], MinOf[T], and so on.
For types, which require the parameter list, that kind of works.
For functions, which will often infer it, the result is strange:
there is no obvious difference between math.MinOf(1, 2) and math.Min(1, 2).
Any new, from-scratch, post-generics API would presumably not include the Of - Tree[K,V], not TreeOf[K,V] -
so the "Of" in these names would be a persistent awkward reminder of our pre-generics past.
Another possibility is to adopt some kind of notation for default type parameters.
For example, suppose you could write
The (= ...) sets the default for a type parameter.
The rule for types could be that the bracketed type parameter list may be omitted when all parameters have defaults,
so saying List would be equivalent to List[interface{}], which, if we are careful, would be identical to the current code,
making the introduction of a generic List as list.List not a backwards-incompatible change.
The rule for functions could be that when parameters have defaults,
those defaults are applied just before the application of default constant types in the type inference algorithm.
That way, Min(1, 2) stays a float64, while Min(i, j) for i, j of type int, infers int instead.
The downside of this is a little bit more complexity in the language spec,
while the upside is potentially smoother migration for users.
Like the "Of" suffix, an API with type defaults would be a persistent awkward reminder of our pre-generics past,
but it would remind mainly the author of the code rather than all the users.
And users would not need to remember which APIs need an "Of" suffix added.
Are there other options we haven't considered?
Are there arguments for or against these options that haven't been mentioned?
Thanks very much.
Beta Was this translation helpful? Give feedback.
All reactions