tuples! tuples! tuples!

392 views
Skip to first unread message

Andrew Harris

unread,
Apr 28, 2024, 7:10:02 AMApr 28
to golang-nuts
Bouncing out from some recent discussions on the github issue tracker, it seems like there's some interest in tuples in Go. I thought the discussion in #66651 led to some interesting ideas, but it's also beginning to drift. Maybe this is a better place to brain-dump some ideas. (This could be a proposal but I'm not sure that's quite right either, that might be spammy.)

Some recent issues:
1. #64457 "Tuple types for Go" (@griesemer)
2. #66651 "Variadic type parameters" (@ianlancetaylor)
3. "support for easy packing/unpacking of struct types" (@griesemer)

Synthesizing from those discussions, and satisfying requirements framed by @rogpeppe, the following is a design for tuples that comes in two parts. The first part explores tuples in non-generic code, resembling a restrained version of #64457. The second part explores tuple constraints for generic code, reframing some ideas from #66651 in terms of tuples. It's a fungal kingdom approach, where tuples occupy some unique niches but aren't intended to dominate the landscape.

TUPLES IN NON-GENERIC CODE

Tuples are evil because the naming schemes are deficient. To enjoy greater name abundancy, this design tweaks tuple types from #64457 in the direction of "super-lightweight structs". It still allows tuple expressions from #64457, for tuples constructed from bare values.

1. Tuple types
Outside of generics, tuple type syntax requires named fields.

TupleType = "(" { IdentifierList Type [ ", " ] } ")" .

// e.g.:
type Point (X, Y int)


More irregularly, the TupleType syntax is used exclusively to declare named types, and these named tuple types cannot implement methods. As a result, a named tuple type is entirely defined at the site of the type definition.

2. Tuple literals
The tuple expression syntax of #64457 remains valid. The result is an implicitly typed tuple value. Literals of a named tuple type are also valid, and resemble struct literals.

point1 := (0, 0) // implicitly typed
point2 := Point(X: 0, Y: 0) // explicitly typed



3. Promotion and expansion
There is no way to capture the type of an implicitly typed tuple value - the result of a bare tuple expression - with tuple type syntax. However, promotion and expansion are available as way to leverage tuple values.

- Promotion: An implicitly typed tuple value is freely and automatically promoted to a value of a named tuple type, if and only if the sequence of types is congruent (same types, same order, same arity) between the implicit and named type:

type T (string, string)
var t T
t := ("foo", "bar")


The RHS of the assignment is implicitly typed (string, string), so the value can be promoted to the LHS's congruent type T without further ceremony.

- Any tuple value can, under the condition of congruence, expand with ... "wherever a list of values is expected" (#66651). This means places like assignments, function calls, function returns, struct/slice/array literals, for/range loops, and channel receives. Each of the github issues (#64457, #64613, #66651) explores this in more detail. Qualifications and some subjectivity are involved, and a full proposal would explore this more completely and sharply, but the intuitive notion is pretty straightforward.

TUPLE CONSTRAINTS

For generic code, this design's driving concept is tuple constraints. A tuple constraint describes type sets that are exclusively composed of tuple types. Loosely, where union-of-types or set-of-methods type constraints are currently, a tuple constraint would also be allowed. The rules for code parameterized on tuple constraints should resemble #66651 in many ways. Most essentially, it should be possible to substitute a tuple constraint "wherever a list of types is permitted", as suggested in #66651.

1. Non-variadic tuple constraints

The current TypeParamDecl production is:

TypeParamDecl = IdentifierList TypeConstraint .

Adding tuple constraints can be accomplished by extending TypeParamDecl syntax to include an alternative to the TypeConstraint, a TupleConstraint. Then, a tuple constraint is constructed from TypeConstraint elements.

TypeParamDecl = IdentifierList ( TypeConstraint | TupleConstraint ) .
TupleConstraint = "(" { TypeConstraint [ "," ] } ")" .


Some examples:
[T (any, any)] describes the type set consisting of any 2-ary tuple
[T (K, any), K comparable] describes the type set of 2-ary tuples that begin with a comparable element.

Via tuple -> list-of-types substitution, the following would be equivalent:

func F[K comparable, V any](f func(K, V)) { ... }
func F[KV (comparable, any)](f func(KV)) { ... }


2. Variadic tuple constraints

A variadic tuple constraint is described with an extension to the TupleConstraint production: an optional VariadicTupleElement is appended to it.

TupleConstraint = "(" { TypeConstraint [ "," ] } [ VariadicTupleElement ] ")" .
VariadicTupleElement = "..." TypeConstraint .


The identifier for a variadic tuple constraint may be still be substituted for a list of types. Drawing from use cases discussed in #66651, this leads to function signatures like:

func Filter[V (... any)](f func(V), seq Seq[V]) Seq[V]

func MergeFunc[V (... any)](xs, ys Seq[V], f func(V, V) int) Seq[V]

Additionally, tuple constraints can accommodate multiple variadic type parameters:

func Zip[T0 (... any), T1 (... any)](xs Seq[T0], ys Seq[T1]) Seq[Zipped[T1, T2]]

func Memoize[In (... comparable), Out (... any)](f func (In) Out) func(In) Out

3. Instantiation and unification

Like #66651, variadic type parameters are only instantiated by non-variadic types. Unification of a concrete tuple type with a tuple constraint considers the compatibility of tuple and constraint arity, and compatibility of tuple and constraint elements.

When unifying type parameters, tracking fixed or minimum arity is significant. Note that the fixed arity of a non-variadic tuple constraint and the minimum arity of a variadic tuple constraint is implicit in the notation. For example:

[T (any, any)] -> any 2-ary tuple
[T (any, any, any, ... any)] -> any tuple of arity 3 or greater

The intersection of any two tuple constraints is calculable, composable, and order independent. (Or, at least the arity question has these properties, and I believe the per-element question is a well - as I understand that's an important property of unification currently.)

Further questions

- The inverse of tuple constraint -> list-of-type substitution, inferring a tuple constraint from a list of types, seems tractable. Maybe it's even useful.
- This design doesn't propose ... unpacking for structs, as suggested in #64613. Is something here helpful?
- This design only allows a single trailing variadic element in a tuple constraint. Comments on #66651 explored uses that would require a single leading variadic element. I don't know whether or not this works formally, but it's intriguing.

Brian Candler

unread,
Apr 28, 2024, 8:06:03 AMApr 28
to golang-nuts
A few questions (I'm ignoring generics for now, and have not cross-referenced the other proposals).

1. Is it possible to access the n'th element of an implicitly typed tuple? More generally, how do you unpack such a tuple - other than by assigning to a compatible named tuple type?

2. How does it interact with multiple assignment? Is the following valid, with or without "..." notation?

a, b := (1, 2)

3. Are implicit tuples allowed on the LHS? Are either of the following valid?

(a, b) := (1, 2)
(a, b) := 1, 2

4. How does it interact with functions returning multiple values? If I have a function which returns (string, error) then can I write

t := myfunc()

and is t implicitly a tuple of (string, error) ? Also with explicit types, e.g.

var t (msg string, err error)
t = myfunc()

More generally, does there remain a difference between a function returning multiple values, and a function returning a tuple?

5. Given that you write

type Point (X, Y int)

does this mean that "(X, Y int)" is valid wherever a type is normally specified, e.g.

var a (X, Y int)

This could lead to ambiguity with functions:

function foo() (X, Y int) { ... }

Currently that's a function returning two named values (and "X" and "Y" are available for assignment within the functino body); but under your scheme, "(X, Y int)" is also a single type literal.

I guess you could resolve that ambiguity by requiring:

function foo() ((X, Y int)) { ... }  // function returns a tuple

6. Is a one-element tuple allowed? e.g.

t := (0,)
t := (X int)(0)

7. Do untyped constants work as per normal assignment? I would expect:

t1 := (0, 0)  // OK, implicit type is (int, int)

int32 a, b
t2 := (a, b)  // implicit type is (int32, int32)

type Pair64 (X, Y int64)
var t3, t4, t5 Pair64
t3 = (0, 0)   // allowed: an untyped constant can be assigned to int64
t4 = t1   // not allowed, t1 is implicitly (int, int) and int cannot be assigned to int64 without conversion
t5 = t2   // not allowed, cannot assign int32 to int64

// Casting??
x := Pair64((0, 0))
y := Pair64(t1)
z := (X, Y int64)((0, 0))

8. Observation: you now have two very similar but different concepts, tuples and structs:

type Point1 (X, Y int)
type Point2 struct{X, Y int}

The alternative ISTM would be to have "implicitly typed struct literals", e.g.

var a Point2
a = {1, 2}

That seems to be a smaller change, and I thought I had seen a proposal like this before, but I haven't dug around to find a reference. Are there use cases that your proposal allows, which this doesn't?

Ian Lance Taylor

unread,
Apr 28, 2024, 12:55:31 PMApr 28
to Andrew Harris, golang-nuts
On Sun, Apr 28, 2024 at 4:10 AM Andrew Harris <harr...@spu.edu> wrote:
>
> 1. Tuple types
> Outside of generics, tuple type syntax requires named fields.
>
> TupleType = "(" { IdentifierList Type [ ", " ] } ")" .
>
> // e.g.:
> type Point (X, Y int)
>
> More irregularly, the TupleType syntax is used exclusively to declare named types, and these named tuple types cannot implement methods. As a result, a named tuple type is entirely defined at the site of the type definition.

Thanks for writing this up.

This part of what you wrote seems too similar to a struct. I don't
know whether we will ever add tuples to Go, but I'm certain that if we
add them they must be clearly distinct from structs. If people ever
start wondering "should I use a struct or should I use a tuple," then
we have failed. One of the goals of Go is that it should be a simple
language. One of the characteristics of a simple language is that
people spend their time writing code. They don't spend their time
considering which aspects of a language to use and how.

Go is also intended to be an orthogonal language. That means that a
concept should apply wherever it is meaningful. In particular, in Go,
any named type should be permitted to have methods. To add a
restriction saying that certain kinds of types can't have methods is
to complicate the language and means that people have more to
remember.

Thanks.

Ian

roger peppe

unread,
Apr 28, 2024, 5:24:47 PMApr 28
to Andrew Harris, golang-nuts
I agree with Ian that the named field syntax is too close to structs.

For me, tuples start to make sense in Go mainly because they map
directly to and from the tuple-like values we already
pass to and from functions, and specifically in the context of a variadic
type parameter language feature that lets us name a set of such values
directly.

That is, in my view, if I if I am able to write code that handles a function
that takes an arbitrary number of parameters, it makes sense to be able to store
those values in a variable with a regular data type and use that in
all the ways that one might use any other variable.

To my mind a tuple with unnamed members is the only reasonable choice
for such a data type because function argument and return parameters
are themselves unnamed (†), so any other choice quickly leads to tricky-to-solve
type-compatibility issues.

While tuples can be evil, I don't believe they're inherently evil, any more than
I believe that functions that take more than one argument are inherently evil.

That is, if tuples were added to the language I'd hope that best
practice guides would make it very clear that tuples are appropriate
only in situations when the only information available about the members
is their position.

For example, it seems to me that a function that zips two
iterators together into pairs of values would be no clearer if the
members of the pair were named or not.

I think this kind of situation occurs most often in generic code,
which is why perhaps this might be an OK time to add tuples
to Go.

  cheers,
    rog.

(†) named parameters in the function body itself do not affect the function's type.

--
You received this message because you are subscribed to the Google Groups "golang-nuts" group.
To unsubscribe from this group and stop receiving emails from it, send an email to golang-nuts...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/golang-nuts/32547c6c-a9c0-4f9d-8a27-41e9173ba85dn%40googlegroups.com.

Andrew Harris

unread,
Apr 29, 2024, 3:58:40 AMApr 29
to golang-nuts
Brian:

1: There would be no access to implicitly typed tuple elements. Both implicit and named tuple values would expand with `...`, for example in an assignment like
`x, y := (0, 0)...`. (FWIW, #64457 suggested decimal names for tuple elements `.0`, `.1` etc.)

2: #64457 and #66651 revealed some subjective preferences in discussion - the `...` seem to be unneccessary, but readable. I lean towards thinking ellipsis are useful for any of these proposals.

3: No, no implicit tuples on LHS.

4, 5, 6: The somewhat underhanded trick here would be that the `(int, int)` type literal would only be usable when declaring a named type (`type T (int, int)`), in part to elide these questions. #64457 I think suggested additional parentheses or commas could work to resolve these questions otherwise.

7: I think the assignments match what I would expect. Casting I think would work so long as untyped constant tuples are valid. They would be necessary for something like `var a, b int = (0, 0)...` also.

8: Yeah - this version of tuples is not really orthagonal to structs.

Ian:

Thanks for the feedback. I guess I'm looking at struct splatting from #64613, in combination with variadic type parameters from #66615, as covering similar use cases as this. There's some difference in formal properties. But, I'm convinced the rabbit in the hat is dead for this version of tuples.

Roger:


> For me, tuples start to make sense in Go mainly because they map directly to and from the tuple-like values we already pass to and from functions, and specifically in the context of a variadic type parameter language feature that lets us name a set of such values directly.

This resonates - without variadic type parameters I don't think tuples add much, but I think they are really intriguing for variadic type parameters.


> For example, it seems to me that a function that zips two iterators together into pairs of values would be no clearer if the members of the pair were named or not.

An example of where I think confusion might arise would be: a bare (int, int) type might be row-major or column-major matrix indices, or on-screen coordinates with origin in the top-left corner, or Cartesian coordinates, etc. - logically distinct types that map onto the same tuple, structurally. In this situation I always reach for named types in other languages.

#64457 allowed defining named types, but isn't strict about them ... that's maybe not fatal to the idea of type constraints based on tuple structure, but some version of tuples for non-generic code would have to come with it.

roger peppe

unread,
Apr 29, 2024, 5:24:46 AMApr 29
to Andrew Harris, golang-nuts
On Sun, 28 Apr 2024 at 12:10, Andrew Harris <harr...@spu.edu> wrote:
Bouncing out from some recent discussions on the github issue tracker, it seems like there's some interest in tuples in Go. I thought the discussion in #66651 led to some interesting ideas, but it's also beginning to drift. Maybe this is a better place to brain-dump some ideas. (This could be a proposal but I'm not sure that's quite right either, that might be spammy.)

Some recent issues:
1. #64457 "Tuple types for Go" (@griesemer)
2. #66651 "Variadic type parameters" (@ianlancetaylor)
3. "support for easy packing/unpacking of struct types" (@griesemer)

Synthesizing from those discussions, and satisfying requirements framed by @rogpeppe, the following is a design for tuples that comes in two parts. The first part explores tuples in non-generic code, resembling a restrained version of #64457. The second part explores tuple constraints for generic code, reframing some ideas from #66651 in terms of tuples. It's a fungal kingdom approach, where tuples occupy some unique niches but aren't intended to dominate the landscape.

TUPLES IN NON-GENERIC CODE

Tuples are evil because the naming schemes are deficient. To enjoy greater name abundancy, this design tweaks tuple types from #64457 in the direction of "super-lightweight structs". It still allows tuple expressions from #64457, for tuples constructed from bare values.

1. Tuple types
Outside of generics, tuple type syntax requires named fields.

TupleType = "(" { IdentifierList Type [ ", " ] } ")" .

// e.g.:
type Point (X, Y int)


More irregularly, the TupleType syntax is used exclusively to declare named types, and these named tuple types cannot implement methods. As a result, a named tuple type is entirely defined at the site of the type definition.

FWIW this kind of restriction has no precedent in Go. Currently any type may be unnamed. This restriction seems arbitrary and irregular to me. 

2. Tuple literals
The tuple expression syntax of #64457 remains valid. The result is an implicitly typed tuple value. Literals of a named tuple type are also valid, and resemble struct literals.

point1 := (0, 0) // implicitly typed
point2 := Point(X: 0, Y: 0) // explicitly typed



3. Promotion and expansion
There is no way to capture the type of an implicitly typed tuple value - the result of a bare tuple expression - with tuple type syntax. However, promotion and expansion are available as way to leverage tuple values.

- Promotion: An implicitly typed tuple value is freely and automatically promoted to a value of a named tuple type, if and only if the sequence of types is congruent (same types, same order, same arity) between the implicit and named type:

type T (string, string)
Isn't this illegal according to your TupleType syntax above? 
var t T
t := ("foo", "bar")


The RHS of the assignment is implicitly typed (string, string), so the value can be promoted to the LHS's congruent type T without further ceremony.

- Any tuple value can, under the condition of congruence, expand with ... "wherever a list of values is expected" (#66651). This means places like assignments, function calls, function returns, struct/slice/array literals, for/range loops, and channel receives. Each of the github issues (#64457, #64613, #66651) explores this in more detail. Qualifications and some subjectivity are involved, and a full proposal would explore this more completely and sharply, but the intuitive notion is pretty straightforward.

TUPLE CONSTRAINTS

For generic code, this design's driving concept is tuple constraints. A tuple constraint describes type sets that are exclusively composed of tuple types. Loosely, where union-of-types or set-of-methods type constraints are currently, a tuple constraint would also be allowed. The rules for code parameterized on tuple constraints should resemble #66651 in many ways. Most essentially, it should be possible to substitute a tuple constraint "wherever a list of types is permitted", as suggested in #66651.

1. Non-variadic tuple constraints

The current TypeParamDecl production is:

TypeParamDecl = IdentifierList TypeConstraint .

Adding tuple constraints can be accomplished by extending TypeParamDecl syntax to include an alternative to the TypeConstraint, a TupleConstraint. Then, a tuple constraint is constructed from TypeConstraint elements.

TypeParamDecl = IdentifierList ( TypeConstraint | TupleConstraint ) .
TupleConstraint = "(" { TypeConstraint [ "," ] } ")" .


Some examples:
[T (any, any)] describes the type set consisting of any 2-ary tuple

By analogy with [T map[any]any] I'd expect the "any" there to be the regular "any" type (as used outside generics) rather than the type constraint "any". That is, my intuition would expect this to disallow a (string, string) tuple, but my sense is that that's not what you're suggesting.

In general, a type constraint can be either a regular type (a type set) or an interface type, but this doesn't seem like either, and looks to me like it would significantly complicate the generics specification.
 
[T (K, any), K comparable] describes the type set of 2-ary tuples that begin with a comparable element.

How would you see that as different from [T (comparable, any)] ?
 

Via tuple -> list-of-types substitution, the following would be equivalent:

func F[K comparable, V any](f func(K, V)) { ... }
func F[KV (comparable, any)](f func(KV)) { ... }

Would these two be equivalent too?

func FK comparable, V any](f []func(K, V)) {}

func F[KV (comparable, any)](f []func(KV)) {}
2. Variadic tuple constraints

A variadic tuple constraint is described with an extension to the TupleConstraint production: an optional VariadicTupleElement is appended to it.

TupleConstraint = "(" { TypeConstraint [ "," ] } [ VariadicTupleElement ] ")" .
VariadicTupleElement = "..." TypeConstraint .


The identifier for a variadic tuple constraint may be still be substituted for a list of types. Drawing from use cases discussed in #66651, this leads to function signatures like:

func Filter[V (... any)](f func(V), seq Seq[V]) Seq[V]

func MergeFunc[V (... any)](xs, ys Seq[V], f func(V, V) int) Seq[V]

It sounds like by your explanation here that this code should be valid:

func main() {
        for x := range FSeq(strconv.Atoi) {
                fmt.Println(reflect.TypeOf(x))
        }
}

func FSeq[V (... any)](f func(string) V) iter.Seq[V] {
        return func(yield func(v V) bool)) {
                yield(f("1234"))
        }
}

If it wouldn't be valid, why not? If it's OK, what would it print?

Andrew Harris

unread,
Apr 29, 2024, 5:06:43 PMApr 29
to golang-nuts
> FWIW this kind of restriction has no precedent in Go. Currently any type may be unnamed. This restriction seems arbitrary and irregular to me.

It seems unpopular.


> Isn't this illegal according to your TupleType syntax above?

Further evidence of a wrong idea :)


>> [T (any, any)] describes the type set consisting of any 2-ary tuple

> By analogy with [T map[any]any] I'd expect the "any" there to be the regular "any" type (as used outside generics) rather than the type constraint "any". That is, my intuition would expect this to disallow a (string, string) tuple, but my sense is that that's not what you're suggesting. In general, a type constraint can be either a regular type (a type set) or an interface type, but this doesn't seem like either, and looks to me like it would significantly complicate the generics specification.

Yes - I'm suggesting a third category of constraint. It would be a significant change, and a tradeoff that would add weight to the generics spec. To further detail what I think this involves:

1. The amount of syntax is on the same order as specifying tuples for non-generic code. Introducing tuples is pretty efficient. I'd imagine a sharp, finely tuned framing the tuple->list-of-types substitution adds more cases to existing syntax, on the same order as #64457 (tuples for go) or #66651 (variadic type parameters) would also require. Overall, #64613 (just `...` for structs) seems simpler.

2. On implementation, before considering variadic tuple constraints: There is tuple machinery already in the type system, used for assignments and signatures, but tuples are not first class. Also, we know where and how they fit in the unification algorithm. It's as simple as one could hope: match the arity, then unify the component types. I take this as evidence the idea of a tuple constraint is feasible.

3. On implementation with variadiac tuple constraints: So long as variadicity is limited to a single, trailing tuple component, the corresponding step in unification - matching arity - is nearly trivial, and unification of types is likewise uncomplicated.

4. Considering varidiac elements of a tuple constraint in positions other than the tail: I don't have a confident analysis of everything that comes to mind, but I'm thinking this leads to bad outcomes, like an increase in the computational complexity of the unification algorithm, or obnoxiously esoteric rules for how constraints intersect.

>> [T (K, any), K comparable] describes the type set of 2-ary tuples that begin with a comparable element.

> How would you see that as different from [T (comparable, any)] ?

The type parameter `K` might be significant elsewhere, e.g. `func F[T (K, any), K any](f func(T), input K)`.

> Would these two be equivalent too?
> func F[K comparable, V any](f []func(K, V)) {}
> func F[KV (comparable, any)](f []func(KV)) {}


Yes, and I can be sharper about "equivalent". I don't think that the type parameter lists should be equivalent. Tuple constraints capture essential information about grouping and position of types. But any slice-of-functions type should be equivalently valid or invalid for either.

func main() {
        for x := range FSeq(strconv.Atoi) {
                fmt.Println(reflect.TypeOf(x))
        }
}

func FSeq[V (... any)](f func(string) V) iter.Seq[V] {
        return func(yield func(v V) bool)) {
                yield(f("1234"))
        }
}


If it wouldn't be valid, why not? If it's OK, what would it print?

My understanding is that how helpful type inference can be is not a question of specification. I would hope the code successfully infers without hints, and prints `(int, error)`. If not, by #64457 rules the hint would be `FSeq[(int, error)](strconv.Atoi)`; my pedantry would suggest spelling out the semantics `type R (Value int, Err error)` and `FSeq[R](strconv.Atoi)`. (I'm losing conviction on that pedantry...)

roger peppe

unread,
Apr 30, 2024, 11:12:09 AMApr 30
to Andrew Harris, golang-nuts
On Mon, 29 Apr 2024 at 22:06, Andrew Harris <harris...@gmail.com> wrote:
2. On implementation, before considering variadic tuple constraints: There is tuple machinery already in the type system, used for assignments and signatures, but tuples are not first class. Also, we know where and how they fit in the unification algorithm. It's as simple as one could hope: match the arity, then unify the component types. I take this as evidence the idea of a tuple constraint is feasible.

Note added emphasis above.


func main() {
        for x := range FSeq(strconv.Atoi) {
                fmt.Println(reflect.TypeOf(x))
        }
}

func FSeq[V (... any)](f func(string) V) iter.Seq[V] {
        return func(yield func(v V) bool)) {
                yield(f("1234"))
        }
}


If it wouldn't be valid, why not? If it's OK, what would it print?

my pedantry would suggest spelling out the semantics `type R (Value int, Err error)` and `FSeq[R](strconv.Atoi)`.

I don't really understand how that could work, tbh. In `FSeq[R]` it seems like you're passing a single type parameter of type R,
so the V type parameter would have a single member of type R. But then that arity (1) wouldn't match the arity of the
argument count to strconv (2).

That is, given `FSeq[R]`, I'd expect it to take an argument of type `func(string) R` not `func(string) (int, error)`
but if that's the case, the example wouldn't work AFAICS.

It seems to me that this conflation of sometimes-named tuples and value-lists is a significant flaw in the suggested semantics.

  cheers,
    rog.

--
You received this message because you are subscribed to the Google Groups "golang-nuts" group.
To unsubscribe from this group and stop receiving emails from it, send an email to golang-nuts...@googlegroups.com.

Andrew Harris

unread,
Apr 30, 2024, 5:56:56 PMApr 30
to golang-nuts
func main() {
        for x := range FSeq(strconv.Atoi) {
                fmt.Println(reflect.TypeOf(x))
        }
}

func FSeq[V (... any)](f func(string) V) iter.Seq[V] {
        return func(yield func(v V) bool)) {
                yield(f("1234"))
        }
}

>>> If it wouldn't be valid, why not? If it's OK, what would it print?

>> my pedantry would suggest spelling out the semantics `type R (Value int, Err error)` and `FSeq[R](strconv.Atoi)`.

> I don't really understand how that could work, tbh. In `FSeq[R]` it seems like you're passing a single type parameter of type R, so the V type parameter would have a single member of type R. But then that arity (1) wouldn't match the arity of the argument count to strconv (2).

> That is, given `FSeq[R]`, I'd expect it to take an argument of type `func(string) R` not `func(string) (int, error)` but if that's the case, the example wouldn't work AFAICS.

> It seems to me that this conflation of sometimes-named tuples and value-lists is a significant flaw in the suggested semantics.

Formally, I'd argue `R` is a valid instantiation of the tuple constraint `(... any)`. `(... any)` is the entire universe of tuples, and `R` is a tuple type. Additionally, Go already recognizes the two tuples in a signature (https://github.com/golang/go/blob/master/src/go/types/signature.go) `func (string) (int, error)` on a more implicit basis.

There is an unstated principle that follows from a notion of promotion. If unnamed->named tuple type promotion is free and automatic, it makes sense to freely unify any occurrences of implicit tuple type `(int, error)` with `R`, but not to unify two congruent named types. I'd thought about this, but a quirk I didn't consider is that a type parameter might unify to a named type in composition, but use an implicit type at the point of instantiation - I think it's justifiable to just promote down to the implicit type in this case.

I wonder that this might be a question that #64457 would run into as well, if it were open, because it also allowed named tuple types.

---

A tangent on what's formally possible, currently: https://go.dev/play/p/7JRCfkjNpJk
We can write:
- A function `F`
- that accepts as input, a function `f`
- accepting 1 string and returning 2 things (`T`),
- parameteric over they types of 2 things (`K` and `V`)
- and prints the structural signature type.

This does parameterize on a single type `T`, with some additional complication bridging (3) and (4): a type `KV`, itself parametrized on `K` and `V`, leveraging the fact that tuples already exist in the type system as second-class components of function signatures.

Without tuple constraints:
1. `func F[T KV[K, V], K any, V any](f T)`

With tuple constraints:
2. `func F[T (any, any)](f func(string) T)` // not variadic
3. `func F[Out (... any)](f func(string) Out)` // variadic tuple constraint; this matches the formal limit of #66651
4. `func F[In (... any), Out (... any)](f func(In) Out)` multiple variadic tuple constraints
Reply all
Reply to author
Forward
0 new messages