Skip to content
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: Allow interfaces to be inferrable generic types #69822

Closed
2 of 4 tasks
glossd opened this issue Oct 9, 2024 · 16 comments
Closed
2 of 4 tasks

proposal: spec: Allow interfaces to be inferrable generic types #69822

glossd opened this issue Oct 9, 2024 · 16 comments
Labels
generics Issue is related to generics LanguageChange Suggested changes to the Go language LanguageChangeReview Discussed by language change review committee Proposal TypeInference Issue is related to generic type inference
Milestone

Comments

@glossd
Copy link

glossd commented Oct 9, 2024

Go Programming Experience

Experienced

Other Languages Experience

Java, JS

Related Idea

  • Has this idea, or one like it, been proposed before?
  • Does this affect error handling?
  • Is this about generics?
  • Is this change backward compatible? Breaking the Go 1 compatibility guarantee is a large cost and requires a large benefit

Has this idea, or one like it, been proposed before?

It's been asked on Reddit once.

Does this affect error handling?

No

Is this about generics?

Yes

Proposal

I don't understand why type interface isn't automatically inferred.
This is a literal example of the error code CannotInferTypeArgs

func f[T any]() {}
func _() {
    f() /* ERROR "cannot infer T" */
}

To understand the problem I decided to play around with the Go compiler and, surprisingly, made it work. I sent the changes for review
For this demonstration, I limited the number of TypeParam to only 1 for a possible infer, because I can see how it can complicate things in cases such as f[T any, S [T]](){}. I also limited the interface type to a basic one
If you consider having interfaces as inferrable generics, I'll be happy to contribute. First improvement is to allow multiple generic parameters to be inferred. My idea is to only infer the generic type if it's not used in any other generic parameters. Then it would sound good.
I'd be happy to hear any feedback.
Thank you for your time.

Language Spec Changes

The Go compiler would allow the type interface to be inferred.

Informal Change

This code compiles

func f[T []any]() {}
func _() {
    f()
}

This one doesn't

func f[T any]() {}
func _() {
    f()
}

It'd be great to infer T as any

Is this change backward compatible?

Yes, because now you'd have to specify the interface for code to compile.

Orthogonality: How does this change interact or overlap with existing features?

It expands the types for inferrable generics.

Would this change make Go easier or harder to learn, and why?

Yes. Not specifying generics when you don't need to is an improvement

Cost Description

I assume only an if block in the infer function.

Changes to Go ToolChain

src/cmd/compiler

Performance Costs

As far as I see now, the final solution would have O(n) for generic parameters, which are in 99% of cases less than 3

Prototype

https://go-review.googlesource.com/c/go/+/618855

@glossd glossd added LanguageChange Suggested changes to the Go language LanguageChangeReview Discussed by language change review committee Proposal labels Oct 9, 2024
@gopherbot gopherbot added this to the Proposal milestone Oct 9, 2024
@glossd glossd changed the title proposal: spec: proposal title Allow interfaces to be inferrable generic types proposal: spec: Allow interfaces to be inferrable generic types Oct 9, 2024
@adonovan
Copy link
Member

adonovan commented Oct 9, 2024

@ianlancetaylor @griesemer

@adonovan adonovan added the generics Issue is related to generics label Oct 9, 2024
@griesemer
Copy link
Contributor

In your example

func f[T any]() {}
func _() {
    f() /* ERROR "cannot infer T" */
}

why is inferring any for T correct? Why not int, string, what-have-you? In this case whatever type you choose would work, because T is never used. That is an esoteric case that's not of much practical use.

If T is used inside the function body, inferring any is likely wrong.

It's not clear what the proposal actually is. A single example doesn't make a proposal.

@ianlancetaylor ianlancetaylor added the TypeInference Issue is related to generic type inference label Oct 9, 2024
@glossd
Copy link
Author

glossd commented Oct 9, 2024

@griesemer right, so as of practical use, in my project I have a generic function which returns a struct with that generic. That struct would return a different response, one of which is the generic type. Here's a simple code example.

func Build[T any]() Response[T] { panic(0) }

type Response[T any] struct {}

func (Response[T]) As() T { panic(0) }

func (Response[T]) AsMap() map[string]any { panic(0) }

func (Response[T]) AsString() string { panic(0) }


func main() {
	Build().AsString() // ERROR: cannot infer T 
}

Some of the responses don't need the generic type, but the method As does.
That's all I have. Maybe there are other use cases. But if you say it's unsafe, I'll accept, you wrote all the code for the generics after all:)

@glossd
Copy link
Author

glossd commented Oct 9, 2024

Oh, there's another example from the Reddit post

package main

import (
	"fmt"

	"golang.org/x/exp/slices"
)

// equal is simply ==.
func equal[T comparable](v1, v2 T) bool {
	return v1 == v2
}

func main() {

	s1 := []int{11, 22, 33}
	s2 := []int{11, 22, 33}

	// fmt.Println(slices.EqualFunc(s1, s2, equal))   DOESN'T WORK!!

	fmt.Println(slices.EqualFunc(s1, s2, equal[int]))
}

@fzipp
Copy link
Contributor

fzipp commented Oct 9, 2024

@glossd
But it does work: https://go.dev/play/p/Gckb0wGXfE7

@glossd
Copy link
Author

glossd commented Oct 9, 2024

@fzipp right, sorry, I forgot it's an old post

@findleyr
Copy link
Member

findleyr commented Oct 9, 2024

Another potential problem with this proposal is that not every constraint interface is allowed outside of constraint position. Without something like #57644, it would be irregular that this inference can only succeed if the constraint is permitted as an ordinary interface.

@glossd
Copy link
Author

glossd commented Oct 9, 2024

@findleyr right, that's why I limited the interface to a basic one

@gopherbot
Copy link
Contributor

Change https://go.dev/cl/618855 mentions this issue: compiler show case: allow a single interface as a TypeParam to be inferrable. If you consider having interfaces as inferrable generics, I'll be happy to write more code and handle multiple TypeParam to be inferrable.

@DeedleFake
Copy link

@griesemer right, so as of practical use, in my project I have a generic function which returns a struct with that generic. That struct would return a different response, one of which is the generic type. Here's a simple code example.

func Build[T any]() Response[T] { panic(0) }

type Response[T any] struct {}

func (Response[T]) As() T { panic(0) }

func (Response[T]) AsMap() map[string]any { panic(0) }

func (Response[T]) AsString() string { panic(0) }


func main() {
	Build().AsString() // ERROR: cannot infer T 
}

Some of the responses don't need the generic type, but the method As does. That's all I have. Maybe there are other use cases. But if you say it's unsafe, I'll accept, you wrote all the code for the generics after all:)

If you infer any here and either Build() or AsString() does anything with T, you're opening the door to accidental bugs to save five keystrokes. Not manually specifying a type when there's nothing to infer it from should be a bug. For example, does it make sense for an unspecified reflect.TypeFor() call to default to return a reflect.Type representing any? And that function calls would only do that for any would be even more surprising.

@glossd
Copy link
Author

glossd commented Oct 10, 2024

@DeedleFake that's a good point, you'd want reflect.TypeFor() to fail to compile without the generic type. Without the type, the function completely loses its purpose. But it won't restrict the generic only for any type, you can still specify your type.

@griesemer
Copy link
Contributor

@findleyr zoomed in on the essence of this proposal. The idea is that if there are no type arguments (or no "constrained" arguments - see below), a correct and most relaxed argument type is the type parameter constraint itself. And, in the general case, inference would compute the (type set) intersection of the provided (= "constrained") type argument and the respective constraint. This would allow more powerful inference.

We have dicussed such an approach internally in the past. It seems conceptually not difficult, but possibly non-trivial to implement (for one, we don't have a general type set intersection mechanism implemented), and the devil is in the details. It's also not clear that it will make a big difference in practice. Our "intersection" mechanism at this point is very simple: we just use the argument type if present, or the constraint type if its core type is a single type.

And, as @findleyr mentioned, this all would require that generalized interfaces can be used as value types, not just constraint types.

@ianlancetaylor
Copy link
Member

If have func New[T any]() T { ... } I absolutely want an error if T is not specified. I definitely don't want it to fall back to New[any].

I think the cases where it should be an error if the type parameter is not specified outweigh the cases where we should default to the type constraint.

@atdiar
Copy link

atdiar commented Oct 10, 2024

To add to @ianlancetaylor, @findleyr and @griesemer this can be subtly wrong.

The typeset of any has a cardinality that is undetermined in a program, just because there are at least many basic types that satisfy it.
Inference should fail.

Compared to the other example []any, since the type constructors in go are invariant, we know that the typeset is a singleton.

(Even if conceptually we could consider it also to be the cross product of slice typeset and any typeset to compute the constraints sets implemented and satisfied by []any. But that's another debate.)

If every type was comparable and assertable, basically implementing all the operations of an interface type, we could consider it. (that would also require to assert interfaces to nil type instead of comparing them to nil, it would be another language).
But in the general case of another interface, it still wouldn't be possible to infer the supertype anyway.
An interface can have so many depending on the whole-program.

So everything works as intended here.

@glossd
Copy link
Author

glossd commented Oct 10, 2024

@ianlancetaylor Yes, it seems to be more important to fail than infer. I'm going to close the issue

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
generics Issue is related to generics LanguageChange Suggested changes to the Go language LanguageChangeReview Discussed by language change review committee Proposal TypeInference Issue is related to generic type inference
Projects
None yet
Development

No branches or pull requests

10 participants