-
Notifications
You must be signed in to change notification settings - Fork 17.9k
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: spec: infer function type parameters from generic interface arguments if necessary #52397
Comments
Thanks. Putting on hold for consideration with other forms of type inference. |
Author background
Intermediate.
C, C++, Java, Python, and several others to varying degrees.
DeedleFake: EbenezerJesuraj's (support/feature-upvoting reply for go-generics based on other languages having it): Similar feature proposal found in TypeScript-Generics:
Not directly, no.
Yes.
Anyone dealing with generics and basic interfaces and more complicated ones that build on top of those.A Project which i am currently working on.
No. |
Any changes here? |
Nothing has changed. |
I thought I'd mention that I work around this issue in my ranges library by having an identity function that accepts the sub type and returns the base type. https://github.com/w0rp/ranges/blob/4c2ae83006ec9f79ef267e10b9794b271ae9fe4e/ranges/primitives.go#L59 Hopefully optimisations will remove calls to the function. |
I was coming here today to propose something similar to what this issue represents. I think what I was going to propose is just more detail towards the same goal, so instead of opening a new proposal I'll add some notes here. Below I first have some notes about concretely how this inference might work, and then some comparisons with similar features in the Rust programming language just as some broader context that might help while evaluating the proposal. EDIT: Some days after writing this I realized that #41176 seems to be covering the same problem and discussion there seems to amount to the same thing I proposed here. I'm sorry I didn't see that issue before posting this. I'm going to leave this here since I already generated whatever notification noise it's going to generate, but I think the discussion over in that other issue is already covering the key pros and cons of what I proposed here. Proposed DesignCurrently I believe that the Go compiler wants to complete type inference first and then separately check whether the values being assigned to any interface-typed arguments are actually implementations of the designated interface. I expect this model means that the current type inference mechanism is largely naive about interfaces, and broadly treats all parameterized types the same. I think this proposal entails having the inference algorithm include a special case for when an argument is a generic type whose underlying type is an interface and where the argument type has at least one type parameter that is a type parameter of the enclosing function. As far as I can tell, this can come as a separate step before the existing specified type inference algorithm: inferring the type parameters for an interface value is a separate step which can conclude before beginning function type parameter inference. To make this more concrete, let's consider an example: type Iterator[T any] interface {
Next() (T, bool)
}
func SliceFromIterator[Elem any](iter Iterator[Elem]) []Elem {
// ...
}
type IntRangeIter struct {
// ...
}
func (r *IntRangeIter) Next() (int, bool) {
// ...
} The goal of the proposal is that it be possible to call iter := &IntRangeIter{ /* ... */ }
slice := SliceFromIterator(iter) To answer that question, the compiler must first answer this question: Does the static type of the given The compiler knows that any implementation of After proving all of the required method names are present, the next step is to try to fit the signatures of those methods to those in the interface. To do this, we can choose any arbitrary method from the interface type's method set (in this case that just
If any of those conditions don't hold, inference fails and this algorithm halts. The final step is then to check whether all of the placeholders
If all of the above steps succeed then we should now have a single concrete type associated with each type parameter of Error reportingIn the above I glossed over what happens when "inference fails". I think an important part of making this understandable will be to make sure the compiler can give actionable error messages for each of the different situations where inference fails.
Appendix: Analogy to Rust(This section is just some additional context, not actually part of the proposal.) Interfaces in Go are roughly analogous to "traits" in Rust. But in Rust there are two different ways to write a trait similar to my trait Iterator<Item> {
fn next() -> Option<Item>
}
fn vec_from_iterator<T>(it: impl Iterator<T>) -> Vec<T> {
// ...
}
struct IntRangeIter {
// ...
}
impl Iterator<i64> for IntRangeIter {
fn next() -> Option<i64> {
// ...
}
}
fn example() {
let iter = IntRangeIter::new();
let vec = vec_from_iterator(iter);
// vec is Vec<i64>
} trait Iterator {
type Item;
fn next() -> Option<Item>
}
fn vec_from_iterator<Iter: Iterator>(it: Iter) -> Vec<Iter::Item> {
// ...
}
struct IntRangeIter {
// ...
}
impl Iterator for IntRangeIter {
type Item = i64;
fn next() -> Option<i64> {
// ...
}
}
fn example() {
let iter = IntRangeIter::new();
let vec = vec_from_iterator(iter);
// vec is Vec<i64>
} The first example above seems the closest in syntax to the Go example I gave earlier, because the trait itself has a type parameter. In the second example the This difference is meaningful in Rust because in the first case it's possible for another type to implement the trait for multiple different Item types at once: impl Iterator<i64> for IntRangeIter {
fn next() -> Option<i64> {
// ...
}
}
impl Iterator<i32> for IntRangeIter {
fn next() -> Option<i32> {
// ...
}
} In the second case struct IntRangeIter<T> {
}
impl<T> Iterator for IntRangeIter<T> {
type Item = T;
fn next() -> Option<T> {
// ...
}
} ...but now the type must be decided by the caller of After playing with this idea a little, I've concluded that what I proposed for Go above is actually more like the associated types approach in Rust, rather than making the trait itself generic, because:
At first this concerned me but I've become comfortable with it because it feels consistent with other decisions in Go: Interface implementation is implicit in Go, but trait implementation is explicit in Rust. It seems reasonable then that an implementation of associated types (effectively what I proposed above) could include automatic inference of the associated types, whereas Rust requires explicitly assigning them. My position is that Go's existing support for generic interfaces is already equivalent to Rust's idea of associated types, for the reasons given above, and that this is just adding the automatic inference for the associated types and for functions that use interfaces that have associated types. Go does not currently have anything equivalent to generic traits in Rust, and that feels okay to me. Adding something equivalent to that would be a significant departure from the model of implicit interface implementation (it would require somehow allowing a type to have multiple methods of the same name) and so would be an entirely separate proposal. |
I believe this was fixed with #41176. |
Change https://go.dev/cl/499282 mentions this issue: |
For #39661. For #41176. For #51593. For #52397. For #57192. For #58645. For #58650. For #58671. For #59338. For #59750. For #60353. Change-Id: Ib731c9f2879beb541f44cb10e40c36a8677d3ad4 Reviewed-on: https://go-review.googlesource.com/c/go/+/499282 TryBot-Bypass: Robert Griesemer <[email protected]> Reviewed-by: Ian Lance Taylor <[email protected]> Reviewed-by: Robert Griesemer <[email protected]>
Author background
Experienced.
Ruby, JavaScript, Dart, Kotlin, Java, C, Python, and several others to varying degrees.
Related proposals
Probably, considering that type inference has been kept simple purposefully but with an open door to look into it again after generics are out. Generics are out, so...
Not directly, no.
Yes.
Proposal
Currently, the following fails to compile:
This makes dealing with complex type systems where there are overlapping interfaces with optional functionality somewhat annoying.
As an example of this issue coming up in real code, I have a package for state tracking that has a basic interface type that it exports and several types that extend it. Dealing with the extension types is noticeably more awkward than just dealing with the base type because of the lack of inference. For example,
Anyone dealing with generics and basic interfaces and more complicated ones that build on top of those.
Costs
Easier, I think. Fewer rules about how and where type inference works and doesn't work is simpler and easier to deal with.
Unsure. I am not an expert on type systems, but I would guess that this would complicate the implementation somewhat, possibly resulting in compiler slowdown, at least in cases where this specific form of inference is used.
None, I think. Just the compiler.
Possible increase in specific situations, but probably not terrible. I think.
None.
No.
No.
The text was updated successfully, but these errors were encountered: