Variadic Generics

What about using an annotation instead of using lots-of-dots? So that instead of e.g.

struct ZipSequence<...Sequences: Sequence>: Sequence {
  var (...sequences): (Sequences...)
}

func zip<...Sequences: Sequence>(...sequences: Sequences...) -> ZipSequence<Sequences...> {
  return ZipSequence(sequences...)
}

one could more easily write

struct ZipSequence<Sequences: @pack Sequence>: Sequence {
  var sequences: Sequences
}

func zip<Sequences: @pack Sequence>(sequences: S) -> ZipSequence<Sequences> {
  return ZipSequence(sequences)
}

I used @pack because it conveys the fact that Sequences is a "parameter pack" of generic types, but discussion is welcome.

1 Like

cc @Douglas_Gregor

I thought I might tag you in this thread too in case you want to participate in the Swift-level discussion about this feature.

Rather than pack, it might make sense to do @variadic which is inline with other discussions to rename T... function arguments to @variaidc T. Although one is multiple types and the other is a group of the same type.

2 Likes

@variadic might in fact serve the purpose better, and the fact that is shared between the two concepts should not be a problem because in the variadic arguments case it will appear in the function parameter list "( )", in the variadic generics case it will instead appear between the generic argument clause "< >".

This also makes me think that we might not want to allow both kind of variadics in the same expression.

1 Like

Try not to get too caught up with the spelling. We'll almost certainly need some way to identify that we have a parameter pack containing type parameters. Whether that's introduced by ... or @pack or @variadic is less important than how it behaves.

Well, that depends. With your Variadic definition, you almost surely want to be able to implement an initializer, which would likely have a function parameter pack whose types are described by a type parameter pack:

struct Variadic<Types...> {
  init(_ values: Types...) { }
}

Doug

4 Likes

I sorta regret writing the example in that way. Swift shouldn't need the prefix "...", and that definition of items is probably wrong. If we're using "...", that example should likely be written as:

struct Variadic<T...> {
  var items: (T...)
}

Same thing here:

we don't need the prefix "...", and probably don't even want the notion of it in the language. iterators can be described as:

var iterators: (Iterators...)

so we're forming a tuple type from the types in Iterators and storing that.

You are correct; my example here was bogus. I'll take a pass through this part of the generics manifesto so at least it's not as misleading and wrong. Thank you!

This may not work so well in a generic context unless you had a way to say "I know that someTuple has at least one element".

When it comes to tuple splatting, I think what we really want is a way to take a tuple and expand it into N separate arguments, e.g.,

let someTuple = (1, 3.14159, "Hello")
f(someTuple...). // equivalent to f(1, 3.14159, "Hello")

It's plausible that we could integrate that with variadic generics if, say, a "function parameter pack" was represented as a tuple and all tuples could be expanded like that.

Doug

9 Likes

Could the syntax be ambiguous in this case? We today have that func(_ someParam: SomeType...) is a function that accepts a variable number of arguments of the same type SomeType, so:

func foo(_ v: Int...) {
 // v is an array here
}
foo(1, 2, 3, 4) // this is valid Swift

let v: Variadic<Int> // Specialising variadic generic argument with a single parameter "Int"
  = Variadic(1) // This is ok
  = Variadic(1, 2, 3, 4, 5, 6, 7) // This might be unclear to users, but is relatively similar to calling `foo`

let v: Variadic<Int, String> // Specialising variadic generic argument with two parameters "Int" and "String"
  = Variadic((1,"Hello")) // Everything's ok
  // = Variadic((1, "Hello"), (2, "World"), (3, "!")) // a user might not realise this is a valid call to the initialiser!

This is the reason I thought we may drop the "..." to unpack the variadic generic: to me, in a (variadic) generic context the type T declared as <T...> is already a tuple. So there will be no ambiguity in the aforementioned case.
My reasoning holds unless we want or need the syntaxes (T...) and T to actually represent different things - with the first one representing the tuple expansion and the second one (insert suggestion here).

1 Like

I have not given much thought to this argument (tuple splatting / expanding), but your suggestion is very interesting because IIRC this feature is something that we lost when we dropped the concept of a function's parameters being (also) a tuple.

Edit: fixed link to point to Chris's post, not the whole thread.

@Chris_Lattner3 shared his thoughts on my other thread (about potential implementation-level details of this feature), suggesting that we might want to treat a variadic generic like a standard generic type with the addition of being constrained to be a tuple.

This made my think that we may widen / shift the focus a little bit, and instead of variadic generics we might want to introduce variadic tuples altogether. This means that the syntax (...T) is (maybe) not limited to appear in generic contexts but may be used "anywhere" to indicate a tuple of variadic length. In this way a variadic generic type is simply a type which has one (or more?) generic parameters which are variadic tuples, so that with this shift in focus we are at least getting the same functionality for free.

struct Variadic<A, B, (...Collections: Collection)> {
  [...]
  var collections: Collections
}

We have then to decide how code referring to a variadic tuple works. For example, considering the previous example in which Collections is constrained to the Collection protocol, we might allow calls to members of Collection to be applied to the collections variable in order to return a tuple containing the result of applying the member to all of the tuple elements, or in code:

extension Variadic {
  func interestingFunc() {
    // the next statement is equivalent to the following:
    // (collections.0.first, collections.1.first, collections.2.first, ...)
    var firstElements = collections.first
    [...]
  }
}

What do you think?

In my free time I'm trying to write a document to sum up what we can do with Variadic Generics. Currently I'm in researching (just starting) the problem i.e. "what are we trying to solve with VG? Why would we want them?"

The primary and prominent use case I see is "I actually need a variadic function, but one whose arguments are not all equals to the same type T but similar between each other i.e. they are different concrete types conforming to a (set of) protocols".
In code this means the following:

// I want to pass *any number* of *any type* that conforms to Sequence
func zip<Ss: Sequence>(_ sequences: Ss...) -> ZipSequence {
   [...]
}
// This is not allowed because [Int] and [Int: Int] are not the *same* type
// even if they both are Sequence<Int>
// zip([1, 2, 3], [4: 4, 5: 5, 6: 6])

// I want to pass *any number* of KeyPath<T, *any comparable property*>
func sort<T, C: Comparable>(_ array: [T], by cps: KeyPath<T, C>...) {
   [...]
}
// This is not allowed because KeyPath<Person, String> and KeyPath<Person, Int> are not the *same* type
// even if they both are KeyPath<Person, Comparable>
// sort(anArrayOfPeople, by: \.name, \.age)

Can you suggest any other different use case? If possibile, use a real-world example that you or someone else could not solve because of this missing feature. But a made-up example is obviously welcome as well!

2 Likes

In the reactive world, you have the combineLatest operator, which turns several streams of values of various types into a single stream. In a way, it is like an asynchronous zip (or zip + map, when the host language does not support heterogeneous containers like Swift tuples).

Due to the lack of variadic generics, Swift libraries that provide such facilities can't support an arbitrary number of input streams. They must provide explicit support for up to N inputs (N being limited by some arbitrary decision).

RxSwift, for example, currently supports up to 8 inputs: https://github.com/ReactiveX/RxSwift/blob/fc6030ea9fe9b8af0351fe195b4095a9f2339757/Rx.playground/Pages/Combining_Operators.xcplaygroundpage/Contents.swift#L88

5 Likes

ReactiveSwift has a similar operation that allows up to 10 generic parameters: ReactiveSwift/Signal.swift at master · ReactiveCocoa/ReactiveSwift · GitHub

2 Likes

Thank you for your suggestions! The reactive examples are very interesting ones, because they are apparently similar to the zip one but have two main differences:

  • they have a different parameter other than the variadic one, and this parameter comes after in order to allow trailing closure syntax
  • more importantly, the last parameter is a closure, and the shape of its input depends on the number of generic parameters

Very nice catch guys, highlighting the last point was particularly interesting!

More examples!

2 Likes

Another example that takes a different shape: a library I’ve contributed to, Curry, currently defines function currying for up to 21 arguments:

public func curry<A, B, C>(_ function: @escaping ((A, B)) -> C) -> (A) -> (B) -> C {
    return { (a: A) -> (B) -> C in { (b: B) -> C in function((a, b)) } }
}

This is unlike the zip-style function in that it needs to be able to unpack/repack the types involved. It would be quite cool to be able to define this with a single generic function!

12 Likes

That is another interesting example, but unfortunately I can't figure out how VG may help here - it may be the late hour! I can't wrap my head about both how to specify the parameter of the variadic curry function and its body.
The problem is that this function feel somehow / something recursive.

Edit: maybe with head / tail variadic unpacking? With the support of other totally invented stuff:

// Invented syntax:
// #dropLast(T) returns the variadic generic `T` but the last one
// #last(T) returns the last element of the variadic generic `T`
// #expand(v) expands / unpack a variadic *value* into the surrounding context

// base case
func curry<A, B, C>(_ f: @escaping ((A, B)) -> C) -> (A) -> (B) -> C {
  return { a in { b in f((a, b)) } }
}

// recurring case
func curry<...T>(_ f: @escaping (#dropLast(T)) -> #last(T)) -> <# what to put here? #> {
  return { h in curry({ t in f(h, #expand(t)) }) }
}

Added link on the opening post to a partially completed document about this topic!

3 Likes

I’ve been thinking of “#pack

func myFunc<#pack Ts>(#explode(Ts)) -> Int allWhere Ts: BinaryInteger

Over the past days I've been working to update the document, there are still things missing but overall it covers much more stuff now. Let me think how it's going!

Here is the link to current version.

Note: The link in the OP always posts to the latest version.

4 Likes

Another use-case that just came to my mind. I have not seen anyone yet make use of key-path equality and hashability, but it turned out be such a powerful tool in my case. It allows me to create partial or full updates on UI. One current pain point that for every type that triggers an update I have to type manually every possible case to allow statically type-safe forwarding of a set of key-paths.

For example this is one enum that I have in our project.

protocol Drivable: AnyObject {}
extension Drivable {
  typealias Driver = GenericDriver<Self>
  var driver: GenericDriver<Self> { ... }
}

typealias PartialKeyPathSet<Root> = Set<PartialKeyPath<Root>>

enum Paths: Hashable {
  typealias StatusBarDriver = RemoteDeviceStatusBar.Driver
  typealias PreviewDriver = Mode.Preview.Driver
  typealias ControlDriver = Mode.Control.Driver

  case statusBar(PartialKeyPathSet<StatusBarDriver>)
  case preview(PartialKeyPathSet<PreviewDriver>)
  case control(PartialKeyPathSet<ControlDriver>)

  var statusBarSet: PartialKeyPathSet<StatusBarDriver>? {
    if case .statusBar(let set) = self {
      return set
    }
    return .none
  }

  var previewSet: PartialKeyPathSet<PreviewDriver>? {
    if case .preview(let set) = self {
      return set
    }
    return .none
  }

  var controlSet: PartialKeyPathSet<ControlDriver>? {
    if case .control(let set) = self {
      return set
    }
    return .none
  }
}

Ideally I would want to create a variadic enum that implements the most logic already and requires me to only provide the types I want to use.

enum GenericPath<T...>: Hashable where T: Drivable {
  /* case per generic type parameter - made up syntax */
  case ...$(PartialKeyPathSet<T.Driver>)

  /* optional set per generic type parameter to extract the payload */
}

// The above example could be written as:
typealias UpdatePath = GenericPath<RemoteDeviceStatusBar, Mode.Preview, Mode.Control>

// Index case names similar to tuples.
UpdatePath.1([\.self]) // payload for `Mode.Preview` - `PartialKeyPathSet<Mode.Preview.Driver>`

This would remove a ton of boilerplate in my code.

In the current proposal you do mention variadic enums which is great, but I couldn't find a single example on how we would express the cases of the enum with their payloads.

1 Like

Hi there,
I have a question about the examples in step 1:
Would _... also be a thing? Like for extracting the last element of a tuple:

// ... only *last* element
let longTuple = (a: 1, b: 2, c: 3, d: 4, e: 5)
let (_... , last) = longTuple
// last => 5