Pitching The Start of Variadic Generics

I see what you mean, but I wonder if it wouldn't be clearer with a keyword than just dots. I'd find various even clearer than variadic, because it's not just that the arity is flexible. I'd say we need different syntaxes for those case. Maybe various T vs many T.

In a generic T is sort of flexible, but at least it's fixed within the function context. With variadics T won't even stay the same in your own specific context, which I think makes it one level more complex to understand.

1 Like

What about the notation that could be found in other languages proposals?

func foo<T>(x: ..T) { }
1 Like

How about adding types(n) for type parameters declaration?

// declare 3 type parameters at once
func foo<types(3) Types>(t0: Types.0, t1: Types.1, t2: Types.2) {}
foo(t0: true, t1: 42, t2: "Hello")

// declare 3 type parameters and use them at once
func foo<types(3) Types>(values: Types...) { print(values.0, values.1, values.2) }
foo(values: true, 42, "Hello")

When n is an identifier, then treat it as a parameter:

// declare n type parameters and use them at once
func foo<types(n) Types>(values: Types...) { print("length = \(n), values = \(values)") }
foo(values: true, 42, "Hello", 3.14, [true])  // length = 5, values = true, 42, "Hello", 3.14, [true]

Since variadics is the generalization of length from fixed length, unlike generics which is the generalization of type from fixed type, using number here seems more straight way to express variadic generics for me.

1 Like

You could generalize this idea even further by making it a range, however it might become very clunky.

<types(…2) T> // 0 - 2 
<types(2…) T> // 2 - infinity
<types(2 … 2) T> aka. <types(2) T> // 2 
<types T> // 0 - infinity

Having the ability to control the number of types would be cool, but I don‘t think we need that with the initial pitch. However it just signals that this feature would rather need a custom keyword instead of the or * postfix, unless we want to add regex like ranges. T*{2}, T*{2,100}.

1 Like

I’m confused about a couple things:

  1. The proposal says you can’t have more than one declared variadic generic and then uses examples with two. Am I misunderstanding something?

  2. What is the function of this oft-mentioned splat operator? What would it’s return type be?

  3. I also find the overloading of ... confusing. Spelling of T... and T... meaning two different things in the same declaration is a head-scratcher. I prefer the aesthetics of T* but the future extensibility of T[] here.

I’d also like to second @Jumhyn’s suggestion of establishing a convention of pluralizes generic placeholder names for variadic generic types. It definitely makes it clearer.

As I understand it, the "only one variadic type param" is only a restriction on generic types since you parameterize types explicitly:

struct Zip<Xs..., Ys...> { ... }

Zip<A, B, C, D, E>(...) // where do the Xs stop and the Ys begin

But no such explicit parameterization syntax exists for functions, as functions have named parameters so can have their parameters explicitly differentiated. (which is also why we were able to add support for multiple variadic parameters).

However, we allow generic parameters to be inferred in many (most?) cases, so if multiple variadic parameters are allowed in Zip's init it seems reasonable that we could write:

let z = Zip(xs: a, b, ys: c, d, e)

And have the generic parameters to Zip inferred. Then we could worry about the syntax for explicit parameterization later.

We haven't ever gotten around to an explicit parameterization syntax for generic functions, but maybe it's more important for types?

The splat "operator" (as I've been using the term, at least) wouldn't be a proper Swift operator, and its type isn't really representable in Swift (though maybe it could be represented with the right variadic generics design?). It would be a piece of syntax for saying "take this aggregate value and expand it into its constituent parts." For instance, the tuple splat "operator" would let you do this:

func foo(_ x: Int, _ y: String) { ... }

let t = (5, "blah")
foo(t) // error: 'foo' expects two separate arguments
foo(#splat(t)) // implicitly: 'foo(t.0, t.1)'
4 Likes

One merit to use the number is, we can express 'same-length' constraint really clear: <types(n) Foos, types(n) Bars>.
I considered generalizing n as number parameter. Maybe it can be expanded for types like Matrix<Element, row, column>. (Potentially make collision with dependent types, though)

// declare n type parameters and use them at once
func foo<number n, types(n) Types>(values: Types...) {}
// use number as generic parameter
struct Matrix<Element, number row, number column>

Instead of adding range expression, using where clause would be simpler. I think it's enough to express this requirement. (But I think such constraint would be hard to satisfy, especially in generic contexts.)

<types(n) Types> where n <= 2 // limit n in 0...2 

That‘s kinda neat indeed.

Bikeshedding:

<types(n) T, types(m) R> where (2 ..< 10).contains(n), m == n + 1
3 Likes

We might not need to support specific arity variadics, or same type variadics, from the start, but our syntax should be ready for it, so we should probably decide now how these would be spelled.

That was my point above as well. We shouldn‘t pick a design which will not allow us to extend this in the future. The first version can simply mean zero or more types.

6 Likes

Regarding the Arity Constraints, but maybe a different direction from some of the existing discussions:

I wonder if some lightweight algebra/subscript-like syntax might be useful, like:

  • Every element in T... must be a Set of the corresponding element in U.... This also implies that both have the same number of elements:
func allPresent<T..., U...>(haystacks: T..., needles: U...) -> Bool
  where
    T[i] == Set<U[i]>
  • Or, for the "homogenous tuple" example above, every T must be the same type:
struct HomogeneousTuple<T...> where T[i] == T[j] { … }
// or maybe:
struct HomogeneousTuple<T...> where T[i] == T[i+1] { … }
  • Specific arity: T... must contain at least 2 types:
func process<T...>(_ values: T...) where T[1]: Any { … }

In these examples:

  • 1 is a concrete index
  • i represents "any given index"
  • j represents "the index after i, if there is one"
  • i+1 also represents "the index after i , if there is one", but allows for more complex relationships.

For the call site to be valid, the expression must be true for all indices in the type sequence(s).

Edit: Fixed the mistake that DevAndArtist noted below. Thanks!

Your T and U are flipped in your first example description.

Should be:

Every element in `T...` must be a `Set` of the corresponding element in `U`.

While I also like the idea of an index, I still think we need support for Ranges. In your current example it feels like the where clause would become a bit weird if the index isn't introduced earlier:

<T...> where T[i], (2 ... 10).contains(i), T[5] == String

// slightly better
<T*{2,10}> where T[5] == String
3 Likes

Instead of _countTypeSequence(xs), what about

func measure<T...>(_ xs: T...) -> Int {
  xs.#count // "#" already vaguely means "compiler wizardry" to me
}

or, a bit better IMHO, since it's T... itself that dictates the number of elements, not any particular instance of T...

func measure<T...>(_ xs: T...) -> Int {
  (T...).count
}

WRT to the spelling of _mapTypeSequence, what about just using map, but needing to provide a name for the particular type of whichever element you're working with in <>?

func cut<T..., U...>(_ xs: T..., transform:(T...) -> U...) -> U... {
  xs.map<T> { //
    // Not sure if types should need the `.self` suffix here. I vote
    // no, but I don't like that rule in regular Swift anyway.
    switch T {
    case Int: return $0 * 2
    case String: return "Hello, " + $0
    case View: return $0.padding(20)
    case Equatable: return { other: T in $0 == other }
    case CustomStringConvertible: return $0.description
    // ...
    case _: return "\(T.self)"
    }
  }
}
side note

(Wow, that's a lot of "return"s in there! Sure would be nice if the compiler could infer them in instances where a switch is the only statement in a function or closure which is supposed to return something, and the case in question only has a single expression...)

Setting aside the desire to keep this spelled different from the regular map (which this technically does, since map normally has two generic parameters, not one), the <> part also provides a way to specify which type you're referring to if you've nested multiple maps with the same type sequence:

func convolutedExample <T..., U...> (xs: T..., ys: T...) -> U... {
  xs.map<X> { x in
    ys.map<Y> { y in
      if X == Y { return "\(X.self) == \(Y.self), how boring..." }
      // do whatever less boring stuff you want here
    }
  }
}

Anyway, moving on...

What about #?

We could (though perhaps not "should") use those and — we could use * or + to denote “zero or one (respectively) or more of this type”, and postfix to denote “zero or more types”:

struct Foo <Type1: T…, Type2: (U, V…), Type3: (W…)+, Type4: (X+, Y…)*> {
  var t1: Type1 // a heterogeneous sequence of types whose length is >= 0
  var t2: Type2 // a heterogeneous sequence of types whose length is > 0
  var t3: Type3 // a homogeneous sequence whose length is > 0 of heterogeneous type sequences whose lengths are >= 0
  var t4: Type4 // a homogeneous sequence whose length is >= 0 of heterogeneous type sequences, each of which is a homogeneous type sequence whose length is > 0 followed by a heterogeneous type sequence whose length is >= 0
}

I must admit, it is a bit cryptic if you haven't been exposed to regular expressions, but it doesn't seem too hard to explain either (says the guy who first learned about * and + in this context many years ago). I mean, at some point I think we have to accept that code written about complex subjects might have complex syntax -- we just need to avoid making the syntax unnecessarily complex. That said, while this wouldn't bother me personally, I'm not the only person who uses Swift and I wouldn't be even a little bit surprised if someone comes up with a better way.

Type sequence mapping seems unclear. I feel mapping one type sequence to another type sequence is an important use case.


If I want to map T… to the generic constraint of another type, for example, mapping parameter types (A, B, C, …, Z) to return type (Result<A>, Result<B>, Result<C>, …, Result<Z>).

Something like func foo<T…>(bar: T…) -> Result<T…> seems like the return type is Result<(A, B, C, …, Z)>.

Something like func foo<T…>(bar: T…) -> (Result<T>)… to me, does not clearly indicate that T is a variadic type in the return type.


Type sequence mapping could possibly be defined like so:

func foo<T…, U…>(bar: T…) -> U… where U == Result<T>

Note, the where clause does not use the syntax, and instead acts more like a for each U, given some T. I think this is the most clear unless we add some new syntax for type sequence mapping.


Another example of mapping one variadic generic constraint to another variadic generic constraint:

func mapResults<T…, U…, V…>(from requests: T…) -> U… where T == Request<V>, U == Result<V>


Edit: this seems similar to arity constraints, but maybe the proposal should more clearly define how it looks to use a type sequence as a return type.

2 Likes

Your post made me wonder about supporting ranges within the subscript, like:

where T[2...10]: ExpressibleByIntegerLiteral

I'm not sure if that would be useful, but I find it interesting :grinning_face_with_smiling_eyes:


Another thought: My explorations didn't include any enforcement of an upper bound. One way to express an upper bound might be to declare that index to be Never, like…

  • Specific arity: T... must contain at least 2, and no more than 10 types:
func process<T...>(_ values: T...)
  where
    T[1]: Any,
    T[i+10] == Never

That's not especially graceful. And it doesn't strictly prevent the type sequence T... from exceeding 10 members. But it would prevent calling process() with more than 10 values.

<T...> where T[i], (2 ... 10).contains(i), T[5] == String

Ranges are good, but here I think it should say that i is in the range 0..<n, where n is in (2...10).

This seems to be similar to the idea I suggested in my reply above too: Pitching The Start of Variadic Generics - #18 by AliSoftware

TL;DR: My suggested syntax would be:

func debugPrint<T[]>(_ values: T[])
  where T: CustomDebugStringConvertible
{
  print(values.count)
  for v in values { /* v is Any */ }
}
func needAtLeastOne<T[1...]>(_ values: T[]) {
  ...
}
func debugPrintEquivalentSyntax1<T[0...]>(_ values: T[])
  where T: CustomDebugStringConvertible
{ ... }
func debugPrintEquivalentSyntax2<T[...]>(_ values: T[]) // '...' here is the UnboundedRange operator
  where T: CustomDebugStringConvertible
{ ... }

And

public struct ZipSequence<Sequences[]>: Sequence
  where Sequences: Sequence 
{
   ...
   public struct Iterator: IteratorProtocol {
      public typealias Element = Sequences[].Element // maps the 'Sequences' type sequence into a type sequence of their respective 'Element'
   }
}

What I like about this syntax is:

  • It plays well with potential future direction of specifying arity and arity ranges
  • The square brackets makes it more natural (at least to me) to see and read T[0], T[1], T[n] at being different types (and not necessarily all the same like T... can imply due to its ressemblance with the existing variadic parameters feature)
1 Like

Is there really a value in adding ranges? It seems to make more sense defining explicitly typed parameters with trailing variadic generics. I can't think of any example where indexing variadic types is more clear than providing explicit types.

If we want a single string, do something like this: func foo<T...>(a: String, b: T...). This could even cover more complex cases like: func foo<U..., V...>(a: U...) where U == (String, V...).

I'm not quite able to follow your point, but when I was talking about ranges I was referring to the dimension. Personally I only really care about cases with a fixed dimension, which would be something like func f<variadic T[5]>(T) { } but you could imagine wanting something to be defined for a dimension of 3, 4 or 5 for example: func f<variadic T[3...5]>(T) { }.

Sorry, I think I replied to the wrong comment. I think I meant to reply to @DevAndArtist, or whoever started the discussion about defining certain type conformances at certain indices. I was assuming T[5] == String Is trying to bind the element at index 5 to a string, or T[3…5] == SomeProtocol To define conformance for a certain sub range.

I was wondering what advantages this has over creating explicitly typed parameters, or multiple variadic generic groups.

1 Like