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: support type inference on generic structs #61731

Open
matthewmueller opened this issue Aug 3, 2023 · 7 comments
Open

proposal: spec: support type inference on generic structs #61731

matthewmueller opened this issue Aug 3, 2023 · 7 comments
Labels
generics Issue is related to generics Proposal TypeInference Issue is related to generic type inference
Milestone

Comments

@matthewmueller
Copy link

matthewmueller commented Aug 3, 2023

(I couldn't find an existing issue for this, but if there is, feel free to close!)

Problem

There's some inconsistencies in the capabilities of type inference for generics.

With functions, type inference works beautifully:

func Accepts[In, Out any](json func(in In) (Out, error), html func(in In) error) http.Handler {
  // call appropriate function depending on Accepts header
}

type Input struct {}
type Output struct {}

func indexJson(in *Input) (Output, error) {
  // render json
}

func indexHtml(in *Input) error {
  // render html
}

// Usage
router.Get("/", Accepts(indexJson, indexHtml))

Structs also work, but they require you to explicitly pass in the generic types:

type Accepts[In, Out any] struct {
  JSON func(in In) (Out, error)
  HTML func(in In) error
}

func (a *Accepts) ServeHTTP(w, r) {
}

// Usage
router.Get("/", Accepts[*Input, Output]{indexJson, indexHtml})

It feels rather unfortunate that you need to explicitly set the [*Input, Output] when usage and even syntax is otherwise so similar.

Proposal

Support type inference on structs, allowing the following:

router.Get("/", Accepts{indexJson, indexHtml})

When the concrete types in indexJson and indexHtml don't line up, it's a type error.


Thanks for your consideration!

@gopherbot gopherbot added this to the Proposal milestone Aug 3, 2023
@ianlancetaylor ianlancetaylor moved this to Incoming in Proposals Aug 4, 2023
@ianlancetaylor ianlancetaylor changed the title proposal: Support type inference on generic structs proposal: spec: support type inference on generic structs Aug 4, 2023
@ianlancetaylor ianlancetaylor added generics Issue is related to generics TypeInference Issue is related to generic type inference labels Aug 4, 2023
@ianlancetaylor
Copy link
Member

CC @griesemer

This seems to be suggesting that we can use a composite literal with a generic type and infer the type arguments from the elements of the composite literal. Seems doable at first glance. It might make a generic Pair type more useful, as one could write Pair{"a", 1} to get a Pair[string, int]. But I don't know that we actually want a generic Pair type.

We would need to consider all types of composite literals. For example, should this work for []Pair{{1, 2}, {3, 4}} ?

@arvidfm
Copy link

arvidfm commented Jun 7, 2024

This would make generic types so much more ergonomic to work with. I can't count the number of times I've written a function like:

func NewPair[L, R any](left L, right R) Pair[L, R] {
    return Pair[L, R]{Left: left, Right: right}
}

and thought to myself "surely at least one of those [L, R] could be elided". It's even worse if you get into the habit of using more descriptive type parameter names instead of just single letters (which can get a bit unreadable if you have a lot of type parameters, or many different related generic types that expect different kinds of type parameters) - you end up with a lot of repeated characters.

In addition, it would be great if this could be extended to type names in general, and not just literals. For instance, consider:

type Pairer[L, R any] interface {
    Left() L
    Right() R
}

type PairerList[P Pairer[L, R], L, R any] []P

type myPair struct {
    left int
    right int64
}
func (mp myPair) Left() int { return mp.left }
func (mp myPair) Right() int64 { return mp.right }

var myList PairerList[myPairType, int, int64]

It would be nice if that last declaration could instead be written as:

var myList PairerList[myPairType]

since the other type parameters can be unambiguously inferred from the return types of the methods on myPair, just like how this function:

func NewPairList[P Pairer[L, R], L, R any](pairs ...P) PairerList[P, L, R] {
    return pairs
}

can be called as NewPairList[myPairType]() and the other parameters will inferred even without any argument values supplied.

One situation where I've found myself wanting this particularly often is when trying to design APIs that rely on embedding:

type MyNode struct {
    NodeBase[SomeType]
}

where you might want to add some constraints to what types are allowed as type parameters to the embedded type, but where adding those constraints would force the user to type out a bunch of additional type arguments every time, making for a very cumbersome API.

@timothy-king
Copy link
Contributor

timothy-king commented Jun 10, 2024

#61731 (comment) focusing on []Pair literals, constants are going to have some interesting cases.

[]Pair{{1, 2}, {"3", 4}} // string(1) compiles but int("3") does not.
[]Pair{{1, 2}, {3.0, 4}}   // `var _ float64 = 1` and `var _ int = 3.0` are both legal.

Also named types.

type NamedInt int
[]Pair{{1, 2}, {NamedInt(3), 4}

A plausible answer to both seem like the types/default types must be identical for inference or it is rejected. [edit: I am not that confident we should reject these examples. The examples mostly just point to real challenges.]

@arvidfm
Copy link

arvidfm commented Jun 10, 2024

Presumably it could work the same as the following does today:

func F[T any](a, b T) {}

func main() {
	// mismatched types untyped int and 
	// untyped string (cannot infer T)
	F(1, "3")
}

F(1, NamedInt(3)) appears to compile, presumably the explicitly typed NamedInt is given priority over the untyped int.

@jimmyfrasche
Copy link
Member

@timothy-king I think these are the only reasonable answers consistent with the rest of the language:

[]Pair{{1, 2}, {"3", 4}} // reject with incompatible types int and string
[]Pair{{1, 2}, {3.0, 4}} // []Pair[float64, int] - mix of float and integer constants default types to float64
type NamedInt int
[]Pair{{1, 2}, {NamedInt(3), 4} // []Pair[NamedInt, int] - each type parameter is independent

@jimmyfrasche
Copy link
Member

I think this invariant is implicit in many of the replies but I'd like to make it explicit: inference on a composite literal must produce the same results as the equivalent function call inference.

For example, with

type Pair[K, V any] struct{/*...*/}

and

func MakePair[K, V any](K, V) Pair[K,V] {/*...*/}

and fixed k, v then the type of the expressions Pair{k, v} and MakePair(k, v) is identical. It would be confusing if they resulted in different types, so I think this is a safe assumption.

If we accept this, we can answer the question about what K and V should be for

[]Pair{{a, b}, {c, d}, {e, f}}

by constructing an equivalent function: a function with the same number and kind of parameters:

func O[K, V any](a K, b V, c K, d V, e K, f V) {}

I believe this approach generalizes to any composite literal.

@jimmyfrasche
Copy link
Member

Here's a runable answer to @timothy-king's questions using that principle: https://go.dev/play/p/czPc_jc-gYK

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

No branches or pull requests

6 participants