Variadic Generics

Some thoughts on the Motivation section and examples based on a quick skim:

  1. It would be more compelling if SwiftUI was given greater prominence (and it was illustrated how variadic generics would solve the problem).

  2. The example for comparison seems weak IMO. For that particular case, the easier solution (without adding any language features) would be to have a 3-way comparison type and extending Comparable with a compare operation that returns a Comparison. One can then combine Comparisons with the natural (left-to-right) monoid operation.

@CTMacUser, @anandabits
In past revisions of the document I've put variadic before the generic parameter name, but in this revision I moved it to (try to?) make it more similar to some and the unreleased any. This decision is obviously not set in stone, and T: variadic Any seems rather awkward to me, too! But I thought that - in general - you will not have unconstrained Variadic Generics, so the "movement" might be worth the awkwardness...

@typesanitizer I started to write the document long ago, before SwiftUI was unveiled, and I wanted to release this update as soon as possible. The SwiftUI section might be updated in a future revision, and why not with the help of Core Team members!

Moreover, IIRC the comparison example was taken from a thread here in the Swift Forums, and that thread mentioned Variadic Generics (again, IIRC). I've almost not updated nor thought about that section or other possibile solutions since the first drafts of the document I think...

variadic seems like a very different modifier than some / any to me.

For instance consider this example:

 T: some Codable
 U: any Codable

The modifier here relates to how T or U relates to Codable.

However in this example:

<V: Codable, variadic W: Codable>

The variadic keyword would not pertain to the nature of the conformance, it relates to what type of entity W is.

10 Likes

Mmmh yeah, in the end I think this was a failed experiment!
I think Iā€™m going to update the document to restore the old version of the keyword.

3 Likes

@technicated thank you for your time and effort in this but I personally have lots of problems with that design and from my own view point variadic generics must be build with some sort of static metaprogramming in mind. I donā€˜t want to sound bad but I think the only things that I agree with is the spelling variadic T and the un-packing into a tuple T.... Everything else makes my head spin.

By the definition of the proposal this should be valid:

struct S<variadic T> {
  let t: T
  init(_ t: T) {
    self.t = t
  }
}

let s_1 = S(42, false)
s_1.t // this should NOT be a collection, so there is no `map` operation here!
s_1.t.0 // this is not valid `variadic T` is not a tuple `T...`

let s_2 = S() // this seems to be equivalent with an empty tuple

We could potentially redefine Tuple as a variadic struct, which was likely mentioned upthread, but it would require two more things.

  • A way to unfold/unpack variadic T into statically safe boilerplate template
  • Generic type parameter labels

That is only needed for tuples. We would shouldnā€˜t forget about enum cases as well.


Brainstorming:

struct Tuple<variadic T> {
  init(_ pack: T) { ... }
}
postfix func ... <variadic T>(_ pack: T) -> Tuple<T> {
  Tuple(pack)
}

func foo<variadic T>(_ pack: T) -> (T...) {
  pack... // calls the above function
}

typealias Void = Tuple<>
2 Likes

Don't worry if you don't agree, this is a delicate subject and worth many discussions!

I'm not sure how to interpret your first code snippet: in this revision of the document where variadic values are Collections and not tuples, s_1.t is a Collection, so you can use map - what do you mean by "this should NOT be a Collection"? Do you prefer t not to be a Collection, or the proposal does not make clear that t actually is a Collection?
Moreover, s_2 is not an empty tuple but a struct whose only member t is some Collection.


Early revisions of the document used tuples and less "dynamism" as the model for Variadic Generics, then someone upthread mentioned that we could directly make variadic values real collections, so I tried to change the document to reflect this suggestion - but we can recover, update and prefer any past revision.

I'm not sure I prefer the Collection approach, but it seemed to solve some issued I had with past revisions and some things I didn't like about them. It was worth a try I think, but maybe there is a third way!

Well I only speak for myself and not for others but I would not want variadic T automatically conform to Collection<.Element == Any, ...>. I think I would prefer this to be a new standalone type which shares some similarities with a tuple, yet itself isnā€˜t a tuple. We could probably refer to its values as 'packs', while what it can and cannot do still needs to be defined. I think that such packs and its type variadic T should be build on top of static guarantees. The only issue is that if we want do something useful with it we either need to transform the pack into a different type, like for example unfolding it into a tuple, or we need some kind of static metaprogramming to tell the compiler to generate statically safe code by iterating over the packā€˜s type and values.


In my above example s_2 would have an empty pack, therefore it would be fairly similar to an empty tuple, which should make them convertible. Thatā€˜s all I was trying to say with that particular example.

mmmmh, this is the key I guess... In Swift we have a very powerful type system and it will be a waste not to use it for this feature. And I think I like better the idea of Variadic Generics friends of tuples more than Collection...

We might also want to seek some guidance from the Core Team!

1 Like

Definitely from @Douglas_Gregor as he co-authored this paper: http://www.jot.fm/issues/issue_2008_02/article2/

But right now he seems to be very busy with function builders, so no idea if variadic generics could be pushed for the next major Apple OS release cycle or not. I would love to see it, even if we could design a roadmap for the final design but start with something simple which would solve some of the common problems we hit in many frameworks today.

2 Likes

I have finally had a chance to get through more of the new draft. Thank you for continuing to work on this very important feature! You've written a lot and I still haven't had time to get through all of the detailed design.

A variadic value can also be passed as the variadic argument of a variadic function (yay!).

Since the members of a variadic value may have different types this will not always be the case. The proposal should probably spell out more clearly exactly when this is expected to work.

// The name of the generic parameter can be used to specify how the
// specialization should be done; note that in this case all the labels are
// specified for clarity but not all of them are really required
let vg3: ComplexVariadic<A: Double, B: Int, T: Int, String, U: String, String>

I was surprised to see generic argument labels get introduced in a comment in the detailed design. This is an important topic that is independent of variadic generics. It will be useful for disambiguation in variadic generics but should probably be an independent proposal.

When we do introduce this feature I think we should consider supporting distinct argument labels. The name you want to introduce into the generic context may not always be the name that makes sense in label position.

+1


Variadic sum types

One very important use case that isn't discussed at all is variadic sum types. Maybe this is because Swift doesn't have structural sum types at all yet. But it probably should (sum types should be on equal footing with product types) and variadic generics are maybe the right place to motivate this topic.

In the document you give an example of the combineLatest operator. In your example, a transform is used to combine the elements. An obvious default transform would be to simply return a tuple. In fact, the combineLatest operator in ReactiveSwift just does this instead of taking a transform. Because tuples are friends with variadic values this tuple-producing operator will be easy to implement in a variadic fashion.

Another variadic operator that might be interesting to have a is a heterogeneous merge operator. In order to implement this in a variadic fashion we would have to have a structural sum type* that is "friends" with variadic values. We cannot rely on the workaround that uses a nominal sum type without also losing the ability to use variadic generics. Having to both introduce a nominal sum type and write out the variadic boilerplate manually will usually mean this operator is just omitted from a library. That is an unfortunate consequence that I hope we can avoid.

4 Likes

+1 I want a variadic Union type, which would also likely push typed throws forward.

Notice that I said sum and specifically not union. This would be the sum type equivalent of a tuple, with cases 1, 2, 3, etc. This is different from a union type. Union types have been deemed too big a risk for type system performance to be considered.

But isnā€˜t a sum type also referred as a tagged union which would be a refinement of a union type?

I specifically donā€˜t want any A | B syntax for that. A tuple like enum with numbered cases corresponding to the index in the variadic pack is enough for me.

I don't know what the syntax for these types would look like. Do you have another idea?

This is exactly what I was suggesting.

What exactly do you mean now? I know that A | B syntax is rejected and Iā€˜m not proposing to reconsider it.

I meant the same, but I didnā€˜t mean to refer to the general 'union types' but rather called the variadic enum Union.

I think I used

(; label0: Type0, Type1, label2: Type2 ;)

for a sum-tuple syntax. We just use semicolons on the inside side of the tuple parentheses. Like regular tuples, an unlabeled entry gets an index number for its access label. (In the above example, the second member gets ".1" as its access case.)

For a broader discussion, I want to cross-link this thread.

1 Like

I don't agree with making variadic items real collections. Variadic items are figments of the compiler's imagination; the object code never sees them, only their post-exploded results (which should be handled the same way as if a developer manually wrote out the same code).

2 Likes

@technicated, I just gave the variadic generics document a read and it really looks great! Thank you for putting all of this together. I'm very excited for what the future has in store for Swift generics.

A couple questions... I was wondering if one will be able to write extensions on variadics (sort of like the project(_:) method AFAIU) like it is a regular protocol conforming to Collection? Also, is there any value in being able to have more fine-grained control over the number of arguments that a given variadic can accept (i.e. restricting a variadic parameter to only take a max number of arguments or a minimum number of arguments)? Something like that would allow you to leave out the first two parameters of you Variadic Generic version of the zip function by restricting the variadic parameter to 2 or more arguments.

I just wanted to bring your attention to a few minor fixes I found while reviewing the current version of your Variadic Generics proposal:

  • Spelling Errors (12):

    • Correct "thells" to "tells" and "wether" to "whether" in comments in collectionApi(t:u:) method in Variadic Value API section.
      // thells wether `T` was specialized with 0 parameters
      // (and therefore `t` contains no elements at all)
      print(t.isEmpty)
      
    • Correct "regualr" to "regular" in last line of Motivation section.

      Let's take a look at some examples that are hard or impossible to "scale up" today with regualr Swift.

    • Correct "clutterness" to "clutter" in Example 3: variadic sorting.

      SwiftUI and its ViewBuilder function builder type suffer from the same problem. ViewBuilder has a fixed amount of buildBlock methods that take C0 , C1 , C2 and so on subviews, leading again to code duplication and clutterness.

    • Correct "toghether" to "together" in Proposed Solution section.
       > This means, for example, that if there exists a function parametrised by a Variadic Generic conforming to the  `Collection`  protocol you can pass to this function an  `Array` , a  `Dictionary`  and a  `String`  - all toghether - because all these types conform to  `Collection` .
      
    • Correct "postifx" to "postfix" in Proposed Solution section.

      A variadic value can also be passed as the variadic argument of a variadic function (yay!). I mildly suggest the operation is again performed by using the ... syntax to explicitly indicate what's happening (but here, much more than in the tuple context, the ... can be mistaken for a postifx operator... suggestions are welcome):

    • Replace first "of" with "as" in the final portion of the Proposed Solution section.

      What can be noted in this example is that the code is practically the same of the current, non-variadic zip implementation. The only real difference is that zip can now be used to zip any number of arbirtary and different sequences together!

    • Correct "arbirtary" to "arbitrary" in the final portion of the Proposed Solution section.

      What can be noted in this example is that the code is practically the same of the current, non-variadic zip implementation. The only real difference is that zip can now be used to zip any number of arbirtary and different sequences together!

    • Correct "previos" to "previous" in link in Introduction section.

      This document is kind of a follow up to a previos one by Austin Zheng - "kind of" because I've read the document, but not too in depth - and this was intentional.

    • Correct "parametrised" to "parameterized" in Proposed Solution section.

      This means, for example, that if there exists a function parametrised by a Variadic Generic conforming to the Collection protocol you can pass to this function an Array , a Dictionary and a String - all toghether - because all these types conform to Collection .

    • Correct "Variadig" to "Variadic" in comments under Declaring and using Variadic Generic functions section.
      // The following code will cause a compile-time error like "Variadig generic
      // argument does not need `...`. Remove it." - a fixit can also be suggested
      func wrongVariadicGenericFunction<T: variadic Any>(ts: T...) { }
      
    • Correct "declarered" to "declared" in comments under Declaring and using Variadic Generic functions section.
      // =============================================================================
      // The `(T...)` syntax is also taken into account, allowing a tuple to be passed
      // as a parameter to a Variadic Generic function. Two functions declarered with
      // both `T` and `(T...)` syntax are different will participate in overload reso-
      // lution.
      // =============================================================================
      
      func overloaded<T: variadic P1>(ts: T) { }
      func overloaded<T: variadic P1>(ts: (T...)) { }
      
    • Correct "trasforming" to "transforming" in first comment under Variadic Value API section.
      // =============================================================================
      // Variadic values conform to the `RandomAccessCollection` protocol and enhance
      // it in various ways. All the standard `Collection` API can be used, and all
      // the standard functionality (for ... in loops, etc) is available. Moreover,
      // all the "trasforming" members are overloaded to return another variadic value
      // instead of an `Array`, and due to how overload resolution works these func-
      // tions will be the preferred one when no type context is given.
      // =============================================================================
      

  • Grammatical Errors (20):

    • Insert comma after "compiler" in comments under the Declaring and using a Variadic Generic section.
      // When there is ambiguity in case of multiple Variadic Generics, the type sys-
      // tem prefers to assign the maximum number of types to the first generic param-
      // eter. The parameter name can be used to manually resolve the ambiguity, and
      // for the happines of the compiler this concept may be generalized to all
      // generic parameters even if this is not strictly needed. This new syntax does
      // not allow the reordering of the generic parameters. The compiler is updated
      // to show new and more detailed informations about generic parameters.
      
    • Hyphenate "in depth" in Introduction section.

      This document is kind of a follow up to a previos one by Austin Zheng - "kind of" because I've read the document, but not too in depth - and this was intentional.

    • Insert comma after "Obviously" in Introduction section.

      Obviously the information contained in the mentioned document and collected on the Swift Forums are going to influence what's presented there.

    • Insert comma after "unfortunately" in first part of Motivation section.

      Today it is impossible or very difficult to express patterns related to an unbounded amount of generic types or values. Maybe the closest thing we have is variadic functions, but unfortunately such functions require all of their arguments to be of the same concrete type

    • Insert comma after "way" in Example 3: variadic sorting.

      In this way animals are sorted by name, and if their name is the same they are sorted by their age, and so on. But if we try to declare the function in the naĆÆve way we get an error:

    • Insert commas before and after "in this case" in Example 3: variadic sorting.

      This example might again seem similar to the zip one. The difference in this case is that Variadic Generics are not used "directly" in the function signature, but are used instead to construct another type that depends on them i.e. KeyPath<Element, T>.

    • Insert comma after "in other words" in Example 5: curry.

      A curried function is one that takes multiple arguments but one at a time - in other words a curried function takes the first argument and returns a new function taking the second argument and so on until all arguments are used and the result is returned.

    • Insert comma after "context" in Proposed Solution section.

      Inside of a generic context a Variadic Generic can be directly used as a type. A value whose type is a Variadic Generic is called a variadic value, and is a collection of zero or more values (more on this later) implementing the Collection protocol:

    • Insert comma after "section" in Detailed Design section.

      In this section we are going to see how Variadic Generics can be declared and used in various contexts.

    • Insert comma after "case" in comments above vg3 declaration in Declaring and using a Variadic Generic section.
      // The name of the generic parameter can be used to specify how the
      // specialization should be done; note that in this case all the labels are
      // specified for clarity but not all of them are really required
      let vg3: ComplexVariadic<A: Double, B: Int, T: Int, String, U: String, String>
      // vg3: ComplexVariadic<A: Double, B: Int, T: <Int, String>, U: <String, String>>
      
      </details>
      
      Insert comma after "because" and ", they" after "generic context" under Variadic Values section.
    • Insert "elements" before "passed" in comments in collectionApi(t:u:) method under Variadic Value API section.
      // Get one of the `P1` passed to `t`
      print(t.randomElement())
          
      // The `Index` type is an `Int` and subscripts are 0-based
      // The same as `t.first` or a nice crash
      print(t[0])
      
    • Insert "`ts`" after "first function" in comments under Declaring and using Variadic Generic functions section.
      func wrongVariadicGenericFunction<T: variadic Any>(ts: T...) { }
      
      // =============================================================================
      // The two functions declared above are somewhat equivalent from the outside be-
      // cause `T` was not constrained in any way. The only difference is that in the
      // second function `ts` is of type `[Any]` while in the first function is of
      // type `variadic Any`.
      
    • Insert commas after "second function", "type `[Any]`", "first function", and "snippet" in comments under Declaring and using Variadic Generic functions section
      func wrongVariadicGenericFunction<T: variadic Any>(ts: T...) { }
      
      // =============================================================================
      // The two functions declared above are somewhat equivalent from the outside be-
      // cause `T` was not constrained in any way. The only difference is that in the
      // second function `ts` is of type `[Any]` while in the first function is of
      // type `variadic Any`.
      //
      // In the following snippet we instead actually have a difference: the second
      // function can inded accept parameters of any type, but only if all of them are
      // of the **same** type! The first function does not have this limitation.
      // =============================================================================
      
    • Insert "and" after "different" in comments under Declaring and using Variadic Generic functions section.
      // =============================================================================
      // The `(T...)` syntax is also taken into account, allowing a tuple to be passed
      // as a parameter to a Variadic Generic function. Two functions declarered with
      // both `T` and `(T...)` syntax are different will participate in overload reso-
      // lution.
      // =============================================================================
      
      func overloaded<T: variadic P1>(ts: T) { }
      func overloaded<T: variadic P1>(ts: (T...)) { }
      
    • Replace "The usage of the" with "Note, the" in comment under Declaring and using Variadic Generic functions section.
      // =============================================================================
      // A Variadic Generic can directly be used as the result type of a function, and
      // it will be automatically considered a `Collection` in concrete code. The us-
      // age of the `(T...)` syntax is used to make the result type a tuple.
      // =============================================================================
      
    • Hyphenate "fixit" in comment under Declaring and using Variadic Generic functions section.
      // The following code will cause a compile-time error like "Variadig generic
      // argument does not need `...`. Remove it." - a fixit can also be suggested
      func wrongVariadicGenericFunction<T: variadic Any>(ts: T...) { }
      
    • Replace "another" with "a" and add comma after "overload resolution works" in first comment under Variadic Value API section.
      // =============================================================================
      // Variadic values conform to the `RandomAccessCollection` protocol and enhance
      // it in various ways. All the standard `Collection` API can be used, and all
      // the standard functionality (for ... in loops, etc) is available. Moreover,
      // all the "trasforming" members are overloaded to return another variadic value
      // instead of an `Array`, and due to how overload resolution works these func-
      // tions will be the preferred one when no type context is given.
      // =============================================================================
      
    • Replace "the other" with "another", remove comma after "fair", insert comma after "However", replace ", and for this reason variadic" with ". For this reason, variadic" in comment under Variadic Value API section.
      // =============================================================================
      // The "transforming" functions need a little bit more attention. We have two
      // overloads of `map`, one that returns an `Array` and the other that returns a
      // variadic value - but those overloads do not allow modifications to the under-
      // lying collection element. And this is fair, because programmers expect a
      // `map` to be pure.
      // However sometimes it might be useful to mutate the original variadic value
      // while creating a new one, and for this reason variadic values offer an origi-
      // nal `project` method.
      //
      // *NOTE:* the name is subject to debate, `project` is the best (and sound) that
      // I could find.
      // =============================================================================
      
    • Remove "Obviously", replace semicolon with comma, and remove comma after "topic" in Introduction section.

      I've never worked too much with variadic generics in my programming life and I wanted to start "fresh" and as unbiased as possible on the topic, to see if I could come up with new or interesting ideas; but it's also possible that this document ends up sharing a lot of similarities with Austin's! Obviously the information contained in the mentioned document and collected on the Swift Forums are going to influence what's presented there.

    • Capitalize "values" in "Variadic values" header in Variadic Values section.
      // =============================================================================
      // Variadic values are somewhat "special", because both inside and outside of a
      // generic context act like a `Collection` - or better *are* a `Collection`. The
      // `Element` of this collection is the constraint of the Variadic Generic. In-
      // side a generic context, the `...` syntax allows users to convert this collec-
      // tion to a tuple whose shape and types will be the concrete types passed to
      // the Variadic Generic parameter.
      //
      

  • Other Errors (2):

    • Add "swift" after "```" to enable syntax highlighting under code block under Variadic Values.
      // =============================================================================
      // Variadic values are somewhat "special", because both inside and outside of a
      // generic context act like a `Collection` - or better *are* a `Collection`. The
      // `Element` of this collection is the constraint of the Variadic Generic. In-
      // side a generic context, the `...` syntax allows users to convert this collec-
      // tion to a tuple whose shape and types will be the concrete types passed to
      // the Variadic Generic parameter.
      //
      
    • In the example function, overloadedApis, under the Variadic Value API section, you make reference to a value, t, that I think you just forgot to add as a parameter to the function.
      func overloadedApis() {
          // No type specified - this is a variadic value of `Any`s
          let aVariadicValue = t.map { $0.someMember }
      
          // A type (`[Any]`) was specified - this is an array of `Any`s
          let anArray: [Any] = t.map { $0.someMember }
          
          // `SubSequence` is another variadic value, whose type is still `variadic P1`
          let subVariadicValue = t.dropFirst()
          
          // This works, subVariadicValue is unpacked and passed to the parameter
          // `values` of the called function
          _ = explicitlyMakeTuple(subVariadicValue)
       }
      

4 Likes

Thank you for the effort put in reading the document!

First, I never thought about the possibility of extending this "variadic value" type, but I think that at least for now it should not be possible.

Your second point is instead very very interesting, but I think it can be deferred to future discussions and can be implemented on top of "bare" Variadic Generics.

And finally, thank you very much for all the errors reported!

2 Likes