golang / go

The Go programming language
https://go.dev
BSD 3-Clause "New" or "Revised" License
122.84k stars 17.51k forks source link

proposal: spec: variadic type parameters #66651

Closed ianlancetaylor closed 4 months ago

ianlancetaylor commented 5 months ago

Proposal Details

Background

There are various algorithms that are not easy to write in Go with generics because they are naturally expressed using an unknown number of type parameters. For example, the metrics package suggested in the generics proposal document is forced to define types, Metric1, Metric2, and so forth, based on the number of different fields required for the metric. For a different example, the iterator adapter proposal (https://go.dev/issue/61898) proposes two-element variants of most functions, such as Filter2, Concat2, Equal2, and so forth.

Languages like C++ use variadic templates to avoid this requirement. C++ has powerful facilities to, in effect, loop over the variadic type arguments. We do not propose introducing such facilities into Go, as that leads to template metaprogramming, which we do not want to support. In this proposal, Go variadic type parameters can only be used in limited ways.

Proposal

A generic type or function declaration is permitted to use a ... following the last type parameter of a type parameter list (as in T... for a type parameter T) to indicate that an instantiation may use zero or more trailing type arguments. We use T... constraint rather than T ...constraint (that is, gofmt will put the space after the ..., not before) because T is a list of types. It's not quite like a variadic function, in which the final argument is effectively a slice. Here T is a list, not a slice.

We permit an optional pair of integers after the ... to indicate the minimum and maximum number of type arguments permitted. If the maximum is 0, there is no upper limit. Omitting the numbers is the same as listing 0 0.

(We will permit implementations to restrict the maximum number of type arguments permitted. Implementations must support at least 255 type arguments. This is a limit on the number of types that are passed as type arguments, so 255 is very generous for readable code.)

type Metric[V... 1 0 comparable] /* type definition */
func Filter[K any, V... 0 1 any] /* function signature and body */
func Filter[K, V... 0 1 any]     /* same effect as previous line */

With this notation V becomes a variadic type parameter.

A variadic type parameter is a list of types. In general a variadic type parameter may be used wherever a list of types is permitted:

A variadic variable or field may be used wherever a list of values is permitted.

Note that a variadic type parameter with a minimum of 0 may be used with no type arguments at all, in which case a variadic variable or field of that type parameter will wind up being an empty list with no values.

Note that in an instantiation of any generic function that uses a variadic type parameter, the number of type arguments is known, as are the exact type arguments themselves.

// Key is a key for a metric: a list of values.
type Key[T... 1 0 comparable] struct {
    keys T
}

// Metric accumulates metrics, where each metric is a set of values.
type Metric[T... 1 0 comparable] struct {
    mu sync.Mutex
    m map[Key[T]]int
}

// Add adds an instance of a value.
func (m *Metric[T]) Add(v T) {
    m.mu.Lock()
    defer m.mu.Unlock()
    if m.m == nil {
        m.m = make(map[Key[T]]int)
    }
    // Here we are using v, of type T,
    // in a composite literal of type Key[T].
    // This works because the only field of Key[T]
    // has type T. This is ordinary assignment
    // of a value of type T to a field of type T,
    // where the value and field are both a list.
    m.m[Key[T]{v}]++
}

// Log prints out all the accumulated values.
func (m *Metric[T]) Log() {
    m.mu.Lock()
    defer m.mu.Unlock()
    for k, v := range m.m {
        // We can just log the keys directly.
        // This passes the struct to fmt.Printf.
        fmt.Printf("%v: %d\n", k, v)

        // Or we can call fmt.Println with a variable number
        // of arguments, passing all the keys individually.
        fmt.Println(k.keys, ":", v)

        // Or we can use a slice composite literal.
        // Here the slice has zero or more elements,
        // as many as the number of type arguments to T.
        keys := []any{k.keys}
        fmt.Printf("%v: %d\n", keys, v)
    }
}

// m is an example of a metric with a pair of keys.
var m = Metric[string, int]{}

func F(s string, i int) {
    m.Add(s, i)
}

Variadic type parameters can be used with iterators.

// Seq is an iterator: a function that takes a yield function and
// calls yield with a sequence of values. We always require one
// value K, and there can be zero or more other values V.
// (This could also be written as Seq[K, V... 0 1 any].)
type Seq[K any, V... 0 1 any] = func(yield func(K, V) bool)

// Filter is an iterator that filters a sequence using a function.
// When Filter is instantiated with a single type argument A,
// the f argument must have type func(A) bool,
// and the type of seq is func(yield func(A) bool).
// When Filter is instantiated with two type arguments A1, A2,
// the f argument must have type func(A1, A2) bool,
// and the type of seq is func(yield func(A1, A2) bool).
func Filter[K, V... 0 1 any](f func(K, V) bool, seq Seq[K, V]) Seq[K, V] {
    return func(yield func(K, V) bool) {
        // This is range over a function.
        // This is permitted as the maximum for V is 1,
        // so the range will yield 1 or 2 values.
        // The seg argument is declared with V,
        // so it matches the number on the left.
        for k, v := range seq {
            if f(k, v) {
                if !yield(k, v) {
                    return
                }
            }
        }
    }
}

In a struct type that uses a variadic field, as in struct { f T } where T is a variadic type parameter, the field must have a name. Embedding a variadic type parameter is not permitted. The reflect.Type information for an instantiated struct will use integer suffixes for the field names, producing f0, f1, and so forth. Direct references to these fields in Go code are not permitted, but the reflect package needs to have a field name. A type that uses a potentially conflicting field, such as struct { f0 int; f T } or even struct { f1000 int; f T }, is invalid.

Constraining the number of type arguments

The Filter example shows why we permit specifying the maximum number of type arguments. If we didn't do that, we wouldn't know whether the range clause was permitted, as range can return at most two values. We don't want to permit adding a range clause to a generic function to break existing calls, so the range clause can only be used if the maximum number of type arguments permits.

The minimum number is set mainly because we have to permit setting the minimum number.

Another approach would be to permit range over a function that takes a yield function with an arbitrary number of arguments, and for that case alone to permit range to return more than two values. Then Filter would work as written without need to specify a maximum number of type arguments.

Work required

If this proposal is accepted, at least the following things would have to be done.

zephyrtronium commented 5 months ago

Aside from the addition of constraints on the count of type parameters, this seems to be a more precise statement of #56462, for some additional background. (cc @DeedleFake)

zephyrtronium commented 5 months ago

Presumably func F[T... 2 1 any]() is illegal. What about func G[T... 1 1 any]()? If that's legal, it feels awkward for it to have a substantially different meaning from func H[T... 0 0 any]().

ianlancetaylor commented 5 months ago

@zephyrtronium Thanks, I entirely forgot about #56462.

func G[T... 1 1 any]() requires a single type argument (which is valid but useless). func H[T... 0 0 any]() permits any number of type arguments. I'm not sure I see the awkwardness.

jimmyfrasche commented 5 months ago

If there were tuple types that looks like it would satisfy the MetricN case and simplify variadic fields/values since you could just wrap any type list in a tuple and not have to define additional machinery.

It seems unfortunate to be limited to a single variadic type as that means you couldn't write, for example, a decorator for any function

diamondburned commented 5 months ago

Would a [V 0..1 any] and [V 0.. any] syntax look clearer? What about [V 0:1 any] and [V 0: any]?

Merovius commented 5 months ago

Another question:

func F[T... any](T) {
    F(T, T) // legal?
}
timothy-king commented 5 months ago

@Merovius Presumably the rules would need to be tightened to not allow calls that generate infinite shapes. The following could also generate an infinite series of func shapes.

func rec[T... 0 0 any](v func(T)) {
   var f func(int, T)
   rec(f)
}
var _ = rec(func(){})

Though the word "compatible" might be doing some heavy lifting to disallow these example.

ianlancetaylor commented 5 months ago

@jimmyfrasche A major goal here is to avoid requiring both Filter and Filter2 in #61898. I don't think a tuple type addresses that.

@Merovius I think that is already prohibited by the rule saying that a generic function may only refer to itself using identical type parameters.

jimmyfrasche commented 5 months ago

@ianlancetaylor I meant that if you also had tuples you could offload the handling of values of type-vectors onto them. However it occurs to me you'd still need some mechanism for writing out the parameters to a closure.

eihigh commented 5 months ago

Variadic type parameters as types of struct fields are significantly more complex to use than those representing variable-length arguments of functions. I suggest initially restricting their use to being available only as function arguments. Even so, it should help to reduce many types, such as Seq2.

EDIT: Is this insufficient?

type StringInt struct { s string; i int }

var m = metrics.Metric[StringInt]{}

func F(s string, i int) {
    m.Add(StringInt{s, i})
}

We should consider #12854 if we want to reduce StringInt.

// With type inferred composite literals:
var m = metrics.Metric[struct{s string; i int}]{}

func F(s string, i int) {
    m.Add({s, i})
}
leaxoy commented 5 months ago

If it is used as a struct field, what type should it be, tuple? Or some other unknown type?

leaxoy commented 5 months ago
type Seq[K any, V... 0 1 any] = func(yield func(K, V) bool)

in addition, why isn't this the case here? @ianlancetaylor

type Seq[E... 1 2 any] = func(yield func(E) bool)
johanbrandhorst commented 5 months ago

I think there's a typo in the Log function example:

    defer [m.mu.Unlock()](about:blank)
Merovius commented 5 months ago

@ianlancetaylor I can't find that rule in the spec? I can find one for generic types, but nothing equivalent for functions.

aarzilli commented 5 months ago

What does PrintZeroes[]() do?

ianlancetaylor commented 5 months ago

@leaxoy

If it is used as a struct field, what type should it be, tuple? Or some other unknown type?

If a struct field has a variadic type, then it is actually a list of struct fields. The types of the listed fields are the list of type arguments.

ianlancetaylor commented 5 months ago

@johanbrandhorst Thanks, fixed. Not sure how that happened.

ianlancetaylor commented 5 months ago

@Merovius Apologies, I'm probably misremembering it. But it seems to me that we need that rule. Otherwise we can write

func F[T any]() string {
    var v T
    s := fmt.Sprintf("%T", v)
    if len(s) >= 1000 {
        return s
    }
    return F[struct{f T}]()
}

And indeed that currently fails to compile:

foo.go:11:8: instantiation cycle:
    foo.go:17:14: T instantiated as struct{f T}
ianlancetaylor commented 5 months ago

@aarzilli

What does PrintZeroes[]() do?

The same thing as fmt.Println(). That is, it does not pass any arguments.

leaxoy commented 5 months ago

The types of the listed fields are the list of type arguments.

Is this list mutable? Does it support iterate and subscript access?

There is a scenario where variable generics are used as function parameters and are expected to be accessible through subscript.

aarzilli commented 5 months ago

@aarzilli

What does PrintZeroes do?

The same thing as fmt.Println(). That is, it does not pass any arguments.

I'm guess that these:

func f1[T... any](x T) { fmt.Println(&x) }
func f2[T... any](x T) any { return x }

the first one is never allowed and the second one is only allowed if called with a single type? It seems weird to me that this introduces variables that aren't real variables and struct fields that aren't real struct fields.

Merovius commented 5 months ago

@leaxoy The proposal intentionally does not propose allowing that. There are undeniably useful abilities, but they are out of scope here.

We do not propose introducing such facilities into Go, as that leads to template metaprogramming, which we do not want to support. In this proposal, Go variadic type parameters can only be used in limited ways.

Merovius commented 5 months ago

@ianlancetaylor I'm a bit worried about the ability to specify bounds on the number of type-parameters, as it seems to me to have the potential to introduce corner cases allowing computation. For that reason, it would be useful to have a relatively precise phrasing what those rules are.

changkun commented 5 months ago
Merovius commented 5 months ago

@changkun

Is f[~T... any] legal?

I'm not sure what that syntax should mean, so I'd say no. Note that F[~T any] isn't legal either.

Is kk := Keys{keys: ???}; for k := range kk.keys {...} legal?

I don't think so (assuming you meant Key[T]{keys}?), as that is a struct type with fields f0, …, fn, there is no kk.keys. Otherwise you'd have to elaborate what Keys is in your example.

Is there an "unpack" to type params in this design?

No.

changkun commented 5 months ago

Note that F[~T any] isn't legal either.

Sorry for the noise. I meant like this f[T... ~int]. Is it legal?

Merovius commented 5 months ago

I don't see a reason why it wouldn't be. Note that the proposal uses Metric[V... 1 0 comparable] as an example and the syntax explanation says f[T... constraint], ISTM that should make clear that any legal constraint can be used.

nobishino commented 5 months ago

If the maximum is 0, there is no upper limit.

I was a little bit confused by the example type Metric[V... 1 0 comparable], because at first glance it seemed as if minimum > maximum.

I prefer following spec/syntax:

leaxoy commented 5 months ago

In addition, using variable generics to solve the problem of seq is not the only way. Another way is to introduce tuple. I have opened a corresponding issue #61920 before, but it was quickly closed.

Consider the following code

type Seq[E any] func (yield func(E) bool)

func MapEntries[K comparable, V any](m M) Seq[(K, V)] {
    for k, v := range m {
        if !yield((k, v)) {
            return
        }
    }
}

Its complexity is much less than that of variadic generics


And answer the previous comment: variadic generics as structure fields can also be tuple types.

atdiar commented 5 months ago

Looks fine to me. I will need to think about it some more. It also shows how extensive a work it requires which is good information. Could allow to tackle a few other things such as the return of zero values.

Quite welcome to simplify iterators too!

A question I have is whether we can have multiple such variadic type parameters, so as to handle more function signatures as constraints. Example function (does it even make sense?)


func StringerifyFunc [A... any, R... any, F f(A) R, S interface{F; fmt.Stringer} ] (f F) S{
    // return a func type with a method that returns the func name
} 

And yes, for variadic fields, maybe there is a way to use a slightly more complex generated field name to further prevent conflicts, for instance, with methods names. (https://go.dev/play/p/qYwY79EG9Ls)

Nice.

DmitriyMV commented 5 months ago

Looks fine to me. I will need to think about it some more. It also shows how extensive a work it requires which is good information. Could allow to tackle a few other things such as the return of zero values.

Quite welcome to simplify iterators too!

A question I have is whether we can have multiple such variadic type parameters, so as to handle more function signatures as constraints. General example that'd denote any function

func[A... any, R... any] (A) R

And yes, for variadic fields, maybe there is a way to use a slightly more complex generated field name to further prevent conflicts, for instance, with methods names. (https://go.dev/play/p/qYwY79EG9Ls)

Nice.

@atdiar One question tho - how do you instantiate this?

I mean:

type GenericFunc[A... any, R... any] func(A) R

type MyFunc GenericFunc[int,int,error] // Will it produce func(int,int,error) or func(int) (int, error) or something else?

How it will decide what type arguments go where?

atdiar commented 5 months ago

Looks fine to me. I will need to think about it some more. It also shows how extensive a work it requires which is good information. Could allow to tackle a few other things such as the return of zero values.

Quite welcome to simplify iterators too!

A question I have is whether we can have multiple such variadic type parameters, so as to handle more function signatures as constraints. General example that'd denote any function

func[A... any, R... any] (A) R

And yes, for variadic fields, maybe there is a way to use a slightly more complex generated field name to further prevent conflicts, for instance, with methods names. (https://go.dev/play/p/qYwY79EG9Ls)

Nice.

@atdiar One question tho - how do you instantiate this?

I mean:

type GenericFunc[A... any, R... any] func(A) R

type MyFunc GenericFunc[int,int,error] // Will it produce func(int,int,error) or func(int) (int, error) or something else?

How it will decide what type arguments go where?

Very good question! I guess that would require parentheses or nested brackets "if" this is valuable and sufficient.

arvidfm commented 5 months ago

A few questions (related to discussions in the previous issue on the topic):

func main() { // a == "hi", b == true, c == 1.2 a, b, c := GetTail(func() (int, string, bool, float64) { return 42, "hi", true, 1.2 }) }

* What would be the result of the following?
```go
func A[T ...any](t T) {
    fmt.Printf("%v", t)
}

func main() {
    A(1, 2, 3)
}

(In my opinion it should be the same as fmt.Printf("%v", 1, 2, 3), so should print 1%!(EXTRA int=2, int=3))


An aside as I don't think it's very realistic and I'm not sure what a good syntax for it would be, but it would be super useful for things like DSLs and builders if it was possible to do define higher-order(?) transforms like:

// A[int, string] -> A(in1 In[int], in2 In[string]) (Out[int], Out[string])
func A[T... any](in In[T]) Out[T] {
    // ...
}

(I realise the above example is syntactically ambiguous, as I said I'm not sure what a good syntax would be!)

That way you could do things like define type-safe Scan-like functions for ORMs (here by declaring typed columns upfront):

type Column[T any] {
    Name string
}

func FetchRow[T... any](conn *sql.Conn, table string, columns Column[T]) (T, error) {
    var result T
    // somehow generate "SELECT id, value FROM table" query
    return result, err
}

func main() {
    // ...
    columnID := Column[int]{Name: "id"}
    columnValue := Column[string]{Name: "value"}
    id, value, err := FetchRow(conn, "table", columnID, columnValue)
}

The main issue here is probably that it would require some way of either iterating over individual elements of the variadic type, or specifying a transform (some kind of map function).

jaloren commented 5 months ago

in the filter example, what happens if someone instantiates the function with two type parameters but the yield func only has a single type A1? is that a compiler error?

bjorndm commented 5 months ago

While this is convenient I am worried that the implementation of type lists might inadvertently end up being Turing complete. The list of types can be used to model the tape of Turning engine, so we will need some severe restrictions to prevent this.

Merovius commented 5 months ago

The list of types can be used to model the tape of Turning engine, so we will need some severe restrictions to prevent this.

How would writing or reading from the tape work and how would the position of the head be encoded? Note that you can't destructure the type list, you can only ever use it as a whole.

I find the idea of Turing completeness for this proposal extremely dubious. I can imagine NP-completeness, but even that seems very tricky.

Either claim should be accompanied at least by the sketch of a reduction proof. See here, for an example of how such a proof can look.

bjorndm commented 5 months ago

I am not claiming the proposed variadic type lists will be Turing complete or NP complete, just stating that I am worried they might be so, or become so due to features added on later. It will probably be best if we have a GOEXPERIMENT implementing this so we can try it out in practice.

ianlancetaylor commented 5 months ago

@aarzilli

I'm guess that these:

func f1[T... any](x T) { fmt.Println(&x) }
func f2[T... any](x T) any { return x }

the first one is never allowed and the second one is only allowed if called with a single type? It seems weird to me that this introduces variables that aren't real variables and struct fields that aren't real struct fields.

You can't take the address of a variadic variable, so f1 is not allowed.

You are only permitted to return a variadic variable if the result type of the function is the same type parameter as that of the variadic variable. That is, return follows the same rule as assignment. So f2 is not allowed.

I agree that it's weird that variadic variables and fields are lists rather than being single entities. I considered always requiring a trailing ... when referring to them, but it cluttered up the code without helping much.

ianlancetaylor commented 5 months ago

@Merovius

I'm a bit worried about the ability to specify bounds on the number of type-parameters, as it seems to me to have the potential to introduce corner cases allowing computation. For that reason, it would be useful to have a relatively precise phrasing what those rules are.

I'm not too worried, but admittedly I would prefer the suggestion that we permit range over a function to take a yield function that takes any number of arguments. If we make that change then we can drop the type parameter bounds, as there only important purpose is to permit the use of range. But that change, while fairly modest from a language perspective, may have a significant impact on the go/ast package and its many friends.

ianlancetaylor commented 5 months ago

@leaxoy I don't understand how tuple types help us write a single version of Filter and Filter2.

ianlancetaylor commented 5 months ago

@jaloren

in the filter example, what happens if someone instantiates the function with two type parameters but the yield func only has a single type A1? is that a compiler error?

Yes. The function argument would not match the signature required by the parameter.

mrwonko commented 5 months ago

Can we apply a transformation to variadic type parameters, such as

type Wrapper[T... any] struct {
    values T
}

func Combine[T... any](elements Wrapper[T]...) Wrapper[T...] {
    // turns (Wrapper[A], Wrapper[B], Wrapper[C]) into Wrapper[A, B, C]
    // (a call with (Wrapper[A, B], Wrapper[C]) would be illegal?)
}

An example of such a Wrapper would be a JS Promise, with Combine being Promise.all. Promises are just an example, I understand they probably don't belong into Go. Maybe an example using channels would be more realistic?

type Tuple[T... any] struct {
    values T
}

func Zip[T... any](elements (chan <- T)...) chan <- Tuple[T...] {
    // turns (chan <- string, chan <- int) into chan <- Tuple[string, int]
}
DmitriyMV commented 5 months ago

@ianlancetaylor

You are only permitted to return a variadic variable if the result type of the function is the same type parameter as that of the variadic variable. That is, return follows the same rule as assignment. So f2 is not allowed.

But how does func SliceOf[T... any](v T) []any works then?

Also imagine this code:

type Callable[V ...comparable] func() V

func callCallable[V ...comparable](callable Callable[V]) V {
    var zero V
    result := callable()
    if result == zero {
        panic("zero result")
    }

    return result
}

func main() {
    callCallable(func() { println("called no result") })
    callCallable(func() int { println("called no result"); return 3 })
}

Does this mean, that for one of those call sites result == zero and below will be generated and for the other it will not?

Similarly, in an array composite literal, if it uses the [...]T syntax.

Wait does this mean we get heterogeneous collections? How do this works in practice?

When calling a function, either a conventional variadic function

I think in practice it means you can only call functions of type func MyFunc(args... any) or func MyFunc(args... any) resultType or with generics that are constrained to the specific type (because there is no way to convert elements of the list from one type to another).

Merovius commented 5 months ago

@DmitriyMV

But how does func SliceOf[T... any](v T) []any works then?

The proposal allows initializing an []any composite literal with a list of values, i.e.

func SliceOf[T... any](v T) []any {
    return []any{v}
}

(which is different from return v).

Does this mean, that for one of those call sites result == zero and below will be generated and for the other it will not?

The proposal text says

A variadic variable or field may be used wherever a list of values is permitted.

That means result == zero is not permitted, because a, b == 42, 23 is not permitted either.

But this is allowed:

func callCallable[V ...comparable](callable Callable[V]) V {
        type x struct{ V }
    var zero x
    result := x{callable()}
    if result == zero {
        panic("zero result")
    }
    return result
}

In this case, an empty type-parameter list would make x an empty struct and the comparison works and is always true.

DmitriyMV commented 5 months ago

@Merovius

What about:

func callCallable[V ...comparable](callable Callable[V]) V {
    result := [...]V{callable()}
    fmt.Println(result)
    return result
}

How this will work?

Merovius commented 5 months ago

No idea. I saw your question and didn't understand how [...]V is supposed to work either, so thought I'd leave that one for @ianlancetaylor.

ianlancetaylor commented 5 months ago

@mrwonko No, there is no way to write the code to do the kinds of transformations you are describing.

ianlancetaylor commented 5 months ago

@DmitriyMV When V is a variadic type parameter, you can't write [...]V. When the proposal mentions the [...]T syntax for an array composite literal, T is an ordinary type or type parameter, not a variadic type parameter. The syntax could be used as, for example, func[E... any](v E) { fmt.Println([...]any{v}) }.

Edit: I updated the proposal text to clarify.

ianlancetaylor commented 5 months ago

@arvidfm

  • Already somewhat touched upon further up, but just to clarify: Is the expectation that the following will be prohibited?
    func A[T... any](t T) {
    // potentially results in an infinite number of instantiations of A?
    A(1, t)
    }

Yes, that should be prohibited.

  • How would this interact with return values? Would the following be allowed? (I'd very much hope so!)
    
    func GetTail[Head any, Tail... any](f func() (Head, Tail)) Tail {
    _, tail := f()
    return tail
    }

func main() { // a == "hi", b == true, c == 1.2 a, b, c := GetTail(func() (int, string, bool, float64) { return 42, "hi", true, 1.2 }) }

Interesting. Yes, I think that would be allowed.

  • What would be the result of the following?
    
    func A[T ...any](t T) {
    fmt.Printf("%v", t)
    }

func main() { A(1, 2, 3) }


(In my opinion it should be the same as `fmt.Printf("%v", 1, 2, 3)`, so should print `1%!(EXTRA int=2, int=3)`)

Yes.

An aside as I don't think it's very realistic and I'm not sure what a good syntax for it would be, but it would be super useful for things like DSLs and builders if it was possible to do define higher-order(?) transforms like:

// A[int, string] -> A(in1 In[int], in2 In[string]) (Out[int], Out[string])
func A[T... any](in In[T]) Out[T] {
    // ...
}

(I realise the above example is syntactically ambiguous, as I said I'm not sure what a good syntax would be!)

That way you could do things like define type-safe Scan-like functions for ORMs (here by declaring typed columns upfront):

type Column[T any] {
    Name string
}

func FetchRow[T... any](conn *sql.Conn, table string, columns Column[T]) (T, error) {
    var result T
    // somehow generate "SELECT id, value FROM table" query
    return result, err
}

func main() {
    // ...
    columnID := Column[int]{Name: "id"}
    columnValue := Column[string]{Name: "value"}
    id, value, err := FetchRow(conn, "table", columnID, columnValue)
}

The main issue here is probably that it would require some way of either iterating over individual elements of the variadic type, or specifying a transform (some kind of map function).

From my perspective that's an example of the kind of template metaprogramming that I want to avoid. I don't think it's a good fit for Go. Just my own opinion, of course.

jimmyfrasche commented 5 months ago

@ianlancetaylor

I'm not too worried, but admittedly I would prefer the suggestion that we permit range over a function to take a yield function that takes any number of arguments. If we make that change then we can drop the type parameter bounds, as there only important purpose is to permit the use of range. But that change, while fairly modest from a language perspective, may have a significant impact on the go/ast package and its many friends.

That's not the only place where number is important.

Consider

func limitedCompose[T... 1 0 any](f, g func(T) T) func(T) T {
  return func(t T) T {
    return g(f(t))
  }
}

without the lower bound that has the invalid corner case |T| = 0:

func limitedCompose(f, g func()) func() {
  return func() {
    return g(f())
  }
}