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 CODETuples 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 typesOutside 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 literalsThe 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 typed3. Promotion and expansionThere 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 constraintsA 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.