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: generic should infer type from variable definition #50285

Open
LeGamerDc opened this issue Dec 21, 2021 · 23 comments
Open

proposal: generic should infer type from variable definition #50285

LeGamerDc opened this issue Dec 21, 2021 · 23 comments
Labels
generics Issue is related to generics Proposal Proposal-Hold TypeInference Issue is related to generic type inference
Milestone

Comments

@LeGamerDc
Copy link

hope go could support infer generic type from variable type

func main() {
	var x Person = get() // to replace var x = get[Person]()
	fmt.Println(x)
}

func get[T any]() T {
	var x T
	return x
}
@gopherbot gopherbot added this to the Proposal milestone Dec 21, 2021
@ianlancetaylor ianlancetaylor added the generics Issue is related to generics label Dec 21, 2021
@ianlancetaylor
Copy link
Contributor

I suppose this would be a little bit like way that the language handles untyped constants, in that the type of the function would be inferred based on how the function call expression is used. But there would be a big difference, which is that untyped constants have a default type, but here the "default type" would be a compiler error.

I'm concerned that the rules for when the type is inferred would be unclear to the person reading the program. People already get confused about untyped constants.

In any case putting this on hold for later consideration after we have more experience with generics.

@leaxoy
Copy link

leaxoy commented Dec 24, 2021

And generic parameter should can be infer lazily.
For example:

m := NewHashMap[int, int]() 
m.Put(1, 2)

can write as:

m := NewHashMap() // currently type parameter is not set.
m.Put(1, 2) // type parameter set to (int, int)

@ianlancetaylor
Copy link
Contributor

@leaxoy That seems very different to me, and should be a separate issue, not this one. Thanks.

@mlevieux
Copy link
Contributor

mlevieux commented Jan 3, 2022

While this is on hold, I'd like to give my two cents on this one:
I don't want type inference to get too smart, and IMO nobody should wish for this. When we'll start having big projects using generics, or complex libraries with functions having a certain number of type parameters, differing in subtle ways, interacting with each other etc... it'll become more and more difficult to read generic code, up to the point where (and that is already the case in a number of languages) it'll be necessary to know a tremendous part of the codebase to know what types you're actually dealing with, their constraints and such. So I left a thumb down, not because I think this is bad in itself, I just feel like it goes against Go's philosophy.

@changkun
Copy link
Member

For the following g and f:

func f[T any]() (x T) { return }
func g[T any](x ...T) { return }

On the call side, because the current type inference is only limited based on function arguments, we can infer g from the return type of f:

g(f[int]()) // OK

But not the vise versa:

g[int](f()) // ERROR: cannot infer T

This seems OK initially since it applies the philosophy of "there is only one way of doing something". However, whenever we trying to do something with ..., it feels so much wierd (am I the only person?):

g(f[int](), f[int](), f[int](), f[int](), f[int]()) // OK
g[int](f(), f(), f(), f(), f())                     // ERROR: cannot infer T

One must write the first style rather than the second. Was there a particular example (similar to other known limitations caused by particular examples) to prevent this during the design cycle of 1.18's generics?

@josharian
Copy link
Contributor

I'm concerned that the rules for when the type is inferred would be unclear to the person reading the program.

My experience so far with type inference is that the rule is:

"Sometimes it works and I'm happy. Sometimes it doesn't work, and I sigh and write out the type."

In any case, the specific case given in the OP is quite clear cut: the type is explicitly named as part of the declaration. Another quite clear cut case (which is the one that brought me here today) is struct literals, where the return type can be inferred from the struct field type.

I suspect that listing and handling just the "obvious" cases would provide a lot of ergonomic support, without complicating the actual rules too much.

@DeedleFake
Copy link

DeedleFake commented Nov 22, 2022

Some simple wrapper types, such as Result[T] or Optional[T], would definitely benefit from being able to tell the type from context when returning. For example, currently if there was an Optional[T] type, you'd have to use it something like this:

type Optional[T any] struct {
  v T
  ok bool
}

func Some[T any](v T) Optional[T] { return Optional[T]{v: v, ok: true} }
func None[T any]() Optional[T] { return Optional[T]{} }

func Example(cond bool) Optional[string] {
  if !cond {
    return None[string]() // Not the most ergonomic.
  }
  return Some(someString()) // Much nicer.
}

It would be a very nice improvement to be able to just do return None() when the type is obvious from the context like this.

@rogpeppe
Copy link
Contributor

rogpeppe commented Nov 23, 2022

How far would this go? How about this?

func main() {
	x := get()
        var y Person = x
	fmt.Println(y)
}

func get[T any]() T {
	var x T
	return x
}

I suspect that something along the lines of the inference proposed for function literals here would probably be reasonable.

But even then, things get tricky I think:

func main() {
	os.Stdout.Write(repeat(100, zero))
}

func repeat[T any](n int, makef func[T]() T) []T {
	s := make([]T, n)
	for i := 0; i < n; i++ {
		s[i] = makef()
	}
	return s
}

func zero[T any]() T {
	return *new(T)
}

Not unexpected, I guess, but the algorithm isn't going to be trivial.

@DeedleFake
Copy link

DeedleFake commented Nov 23, 2022

How far would this go? How about this?

I don't think that's necessary, at least not right now. It could certainly be considered later, but it seems like a lot of complication for a much smaller benefit. I don't know for sure how complicated it would really be, though. It may be possible for the type checker to introduce a temporary type that it can slot in for x, then when it gets to an actual type check, like when assigning x to y, it can see that x has this special fake type and do the inference there, propagating it back to the original assignment. I'm not sure how viable that is, though.

But even then, things get tricky I think:

This seems less complicated to me than the previous example. The usage of repeat() as an argument to Write() means that byte can be inferred for T right there, and then it just becomes byte throughout the implementation of repeat(). I could be wrong, though.

As an aside, the make argument shadows the built-in make(), but you call both in the function.

@rogpeppe
Copy link
Contributor

How far would this go? How about this?

I don't think that's necessary, at least not right now. It could certainly be considered later, but it seems like a lot of complication for a much smaller benefit. I don't know for sure how complicated it would really be, though. It may be possible for the type checker to introduce a temporary type that it can slot in for x, then when it gets to an actual type check, like when assigning x to y, it can see that x has this special fake type and do the inference there, propagating it back to the original assignment. I'm not sure how viable that is, though.

It's definitely viable (Rust does this kind of thing) but not necessarily desirable.

But even then, things get tricky I think:

This seems less complicated to me than the previous example. The usage of repeat() as an argument to Write() means that byte can be inferred for T right there, and then it just becomes byte throughout the implementation of repeat(). I could be wrong, though.

Yeah, I think it should work (if type inference by LHS type is to work), but specifying the rules might not be that straightforward.

As an aside, the make argument shadows the built-in make(), but you call both in the function.

Good catch. Fixed by editing.

@jimmyfrasche
Copy link
Member

I think a single statement is a good natural boundary. So

return None()

would work because None can use the type of the return to fill in the blank but

x := None()
return x

would fail because there's not enough information on the x := None() line.

@sammy-hughes
Copy link

sammy-hughes commented Dec 6, 2022

I think what's certain here is that for an inferred type, it would have to coalesce into a concrete type before it can be used in a concrete way. You pass generically-constrained param to a function that isn't generic, it's a compile-fail. You pass it to a function that is generic, but is constrained differently than the param, it's a compile-fail.

Meanwhile, I agree with the logic on the hold. I do want this long-term, because I DO GET generics, but I also know that this is a common point of confusion. For now, we all have to write adapter handlers/callbacks, which are generic over the structured types which are part of the interface. That's good enough for me. Until generics permit access via common symbols (e.g. array index/struct-fields), which is NOT part of this proposal. we'll still be doing that anyway.

@griesemer
Copy link
Contributor

griesemer commented Mar 31, 2023

Here's an attempt at a more concise formulation of this proposal, at least as how I understand it:

If a generic function is called and the result is used in an assignment (*) to a typed variable, the type of the variable is used for type inference if the function's result type depends on a type parameter. If a function returns multiple results, the same applies for each result.

(*) For the purpose of this proposal, initialization expressions (to fully typed variables) in variable declarations, assignments to redeclared (and thus fully typed) variables in short variable declarations, returning results to function result parameters, and passing values to (user-defined) functions in function calls are all considered assignments where this form of type inference will be applicable.

This form of inference can be viewed as a generalization of #59338: instead of considering the type of the function, we consider the type(s) of the function results for inference.

Some examples:

func f[P any]() P { ... }
var x int = f()  // ok, infers P = int (type of x is int)
x = f()          // ok, also infers P = int (type of x is int)
var y = f()      // not ok, y doesn't have a type yet, so P cannot be inferred

func g[P, Q any]() (P, Q) { ... }
var u, v float64 = g()  // ok, infers P = Q = float64 
x, v = g()              // ok, infers P = int, Q = float64
z, v := g[int]          // ok, P = int (provided), z is int, Q = float64, inferred from v
a, b := g[int]          // not ok, b doesn't have a type yet, so Q cannot be inferred

func h[T any](x, y T) { ... }
h(g[string]())  // ok, T = string, inferred from 1st result of g, Q = string, inferred from y which has type T = string

The h example is a situation where type arguments of both g and h are inferred. This is possible in principle but may be too complicated in practice. It's also not clear what happens if g itself is called with another generic function result, and g is then used as an argument for h, etc. We will need to determine where to draw the line, to keep code and implementation complexity in line.

@griesemer
Copy link
Contributor

Removing this from hold as it is a natural extension of #59338. Also, for better understanding we have prototyped the implementation for simple forms such as the x = f() example above in the dev repo (disabled by default).

For an idea of what's possible in the prototype see $GOROOT/src/internal/types/testdata/examples/inference2.go at tip.

@DeedleFake
Copy link

(*) For the purpose of this proposal, initialization expressions (to fully typed variables) in variable declarations, assignments to redeclared (and thus fully typed) variables in short variable declarations, returning results to function result parameters, and passing values to functions in function calls are all considered assignments where this form of type inference will be applicable.

One more: Sending the result to a channel.

@fzipp
Copy link
Contributor

fzipp commented Mar 31, 2023

z, v := g[string] // ok, P = int (provided), z is int, Q = float64, inferred from v

I guess that should be z, v := g[int].

@griesemer
Copy link
Contributor

@DeedleFake For now I think we may want to constrain this to a limited set of "assignments". Once we add sending results to a channel, we (probably) also need to add setting a map element or providing a map key. Then the question is what about indexing (do we infer an int?), etc. In the cases of channels and maps we have clearly defined types and so it makes sense to include those, but let's do this in a separate round, ounce we are happy with this in the first place.

@griesemer
Copy link
Contributor

The primary issue with this proposal is that in general, type-inference is not easily localized anymore to a single ("flat") assignment/function call. Consider:

func f[P any](x P) P { return x }

var x float64 = f(f(f(0)))

The return type P of the outermost call of f is inferred from the assignment, i.e., P is float. That informs f's argument type, which in turn determines the result type and argument type of the middle f call, and then the innermost f call. The type of 0 will then be inferred to be float64.

In general, this nesting can be arbitrarily deep, with many different generic functions at various instantiation levels. While type inference should be able to infer types in cases like these, our current type checkers are not organized in a way that makes such an approach easily feasible: type checking essentially happens recursively, bottom-up. To make this more general inference work, expressions will need to be type-checked "as a whole" which likely will require significant re-engineering of the type checker.

Putting back on proposal-hold for now.

@LeGamerDc
Copy link
Author

[#58650] solved the problem, should close now

@zigo101
Copy link

zigo101 commented Jan 14, 2024

Solved?

@pat42smith
Copy link

Is this issue really "solved"? My understanding was that #58650 does not implement this; it just makes this easier to implement.

Also, a small modification of the code from the top of this issue does not compile in the playground, not even with the version set to "Go dev branch". https://go.dev/play/p/ry-w07CKoct?v=gotip

@dominikh dominikh reopened this Jan 14, 2024
@griesemer
Copy link
Contributor

This issue is not solved. See this comment for details. Leaving on hold.

@kkqy
Copy link

kkqy commented Jun 7, 2024

I meet the same problem.
I use this pattern to pass variable options.

type Container[T any] struct {
	name  string
	items map[*T]struct{}

	a interface{}
	b interface{}
	c interface{}
}

func (c *Container[T]) Add(item *T) { c.items[item] = struct{}{} }

// ... and so on

// I use type ContainerOption configure the Container.
type ContainerOption[T any] func(*Container[T])

func WithName[T any](name string) ContainerOption[T] { return func(c *Container[T]) { c.name = name } }
func WithA[T any](a any) ContainerOption[T]          { return func(c *Container[T]) { c.a = a } }
func WithB[T any](b any) ContainerOption[T]          { return func(c *Container[T]) { c.b = b } }
func WithC[T any](c any) ContainerOption[T]          { return func(c *Container[T]) { c.c = c } }

func NewContainer[T any](opts ...ContainerOption[T]) bool {
	c := &Container[T]{
		items: make(map[*T]struct{}),
	}
	for _, opt := range opts {
		opt(c)
	}
	return true
}

func main() {
	// the "int" is not meaningful for this statement and it looks very strange.
	NewContainer(WithName[int]("TestContainer"), WithA[int]("bar"), WithB[int]("foo"), WithC[int]("baz"))

	// why not:
	// NewContainer[int](WithName("TestContainer"), WithA("bar"), WithB("foo"), WithC("baz"))
}

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 Proposal Proposal-Hold TypeInference Issue is related to generic type inference
Projects
Status: Hold
Development

No branches or pull requests