Repository: go
Issue: proposal: spec: multi-type type parameters
Labels: LanguageChange,Proposal,generics,LanguageChangeReview

Update
-------

As pointed out by @seankhliao, the pieces of this involving channels were already covered in #41148. However, I think that the generics-related code is still worth discussing. Therefore, this proposal is now _only_ for the `...` constraint detailed below. By limiting the proposal to this constraint, that also partially fixes the `reflect` issue, as

```go
func Example[T ...](v T) {
  reflect.ValueOf(v)
}
```

would be illegal. A variable of a type constrained by `...`, such as `T ...`, would not be allowed to be assigned to anything not also of that exact type `T`, meaning that even passing it to something that takes an `any`, such as `reflect.ValueOf()`, would be illegal.

However, this doesn't fix it completely. In order for this proposal to be useful, things like `type Future[T ...] struct { val T }` would have to be allowed, which would mean that `reflect` could still gain access to the 'variable' via the struct. I'm unsure of how to handle this, though I have a few ideas. One of them is touched on briefly in the original proposal below.

Original Proposal
-----------------

### Author background

- **Would you consider yourself a novice, intermediate, or experienced Go programmer?** Experienced.
- **What other languages do you have experience with?** Ruby, C, Java, Kotlin, Python, JavaScript.

### Related proposals

- **Has this idea, or one like it, been proposed before?** Yes.
  - **If so, how does this proposal differ?** This is _not_ a proposal for general tuples. Instead, it simply extends the idea of multiple returns to apply to more things than just functions.
- **Does this affect error handling?** Not directly, but possibly.
- **Is this about generics?** Yes.
  - **If so, how does this relate to the accepted design and other generics proposals?** It adds some new features.

### Proposal

- **What is the proposed change?**

In Go, functions can return multiple values. This is handled directly, rather than via tuples, the way that a lot of other languages, such as Python and Rust. handle it. Because of this, some expressions in which it would be useful are not capable of yielding multiple 'returns' directly. For example, a user can declare a `chan int`, but not a `chan (int, error)`. Due to the convention of returning `error` from functions directly as their own value, this can lead to some complication when trying to handle errors in other cases, such as when sent from a channel. The common solutions are to either use _two_ channels, one for values and one for errors, or to define a top-level type that can hold the value for you. The first solution is better suited to specific situations, while the second solution mostly solves the problem, but can lead to namespace pollution and other annoyances, especially when the channel operation is happening across package boundaries.

This proposal is _not_ a proposal for general tuples. Instead, this proposal is to extend the usage of multiple 'returns' to other places in which types are specified. These 'tuples' would not be possible to store in a single variable except for in one specific situation, but would instead require that they be destructured immediately, just like how multiple returns from a function works. Unfortunately, this can lead to some ambiguities, so these would have to introduce a small new syntax change to fix, but it would only apply in those caes.

```go
c := make(chan (int, error)) // This would become legal.
v := <-c // This is _not_ legal. There is no real tuple type, so v would not have a valid type.
i, err := <-c // Also not legal due to ambiguity. Is err the bool indicating channel closure? Unclear.
(i, err) := <-c // Legal. This syntax is currently not legal, and it fixes the ambiguity.
(i, err), ok := <-c // Legal. It's now obvious which variable is the bool indicating closure.

// It might make sense to extend the usage to maps and slices, too, but unlike with channels I'm not sure how useful that would be.
c := make(map[string](int, error)) // Key would not be allowed to be multiple types due to lack of comparability.
c := make([](string, int), 0, 10)

// Multiple types would also be available for generics. This is where things get weird, since you can then kind of have variables with
// multiple types. Even limiting multiple types to any doesn't work because you can, for example, still take an address of a variable with
// an any-constrained type. A new pseudo-constraint, here represented by ..., would work similarly to any but would allow multiple types
// and disallow addressing and other complicating operations.
func Lazy[T ...](f func() T) func() T {
  var once sync.Once
  var val T // Would be illegal directly, but is fine here because of the constraints.
  return func() {
    once.Do(func() { val = f() }) // Just works.
    return val
  }
}

getDB := Lazy(OpenDB)
r := getDB() // Illegal, the same as for channels above.
db, err := getDB() // Legal.
```

The biggest complication that I can think of with these changes is how to handle them with `reflect`. I think the simplest approach is to introduce the idea directly into `reflect.Type` with some new methods that would allow you to pull out the types one by one, much like how it currently works for multiple returns to a function. This is potentially a backwards-compatibility issue, as existing code that didn't expect, for example, the element type of a channel to itself have multiple types would break when then given a channel that _does_ have multiple types.

- **Who does this proposal help, and why?**

It would be quite useful for a number of recent proposals. For example, in #56102 the current thinking is to add both `Lazy[T any]` and `Lazy2[T1, T2 any]` just to allow people to use either functions that do or functions that do not return an error. This has happened in a number of other places as well. This proposal would allow that to be replaced `Lazy[T ...]` instead to handle not only those cases but any number of returns from the wrapped function. #56461 is another that would benefit.

- **What would change in the language spec?** Quite a few things. I'm not sure exactly what would be necessary to change.
- **Please also describe the change informally, as in a class teaching Go.** Instead of only functions being able to return multiple values, channels would be able to as well. Similarly, generics would get a special constraint allowing them to do so.
- **Is this change backward compatible?** Yes, except for the possible `reflect` issue detailed above.
- **Orthogonality: how does this change interact or overlap with existing features?** It extends an existing feature to more places.
- **Is the goal of this change a performance improvement?** No.

### Costs

- **Would this change make Go easier or harder to learn, and why?** Harder. It adds some complication in terms of receiving from channels and fetching things from maps in particular, but it also just generally adds costs by adding some extra complexity to channels, maps, slices, and some usages of generics.
- **What is the cost of this proposal? (Every language change has a cost).** Some added complexity to certain types and usages of types.
- **How many tools (such as vet, gopls, gofmt, goimports, etc.) would be affected?** `gopls` and `gofmt` would definitely be affected because they would need to support the new syntax, but it should be relatively minimal.
- **What is the compile time cost?** Some, probably, but not much.
- **What is the run time cost?** None.
- **Can you describe a possible implementation?** No.
- **Do you have a prototype? (This is not required.)** No.