A vision for variadic generics in Swift

Thank you for writing this. I found myself nodding along when reading it, as you’ve covered the (large!) space of features and capabilities that I’d hope for variadic generics, and it fits together well.

The area I’m least convinced of is the use of .element” for getting a pack from a tuple. It's an interesting operation, because it takes a single value and then expands it out into a pack.

For one thing, I think it would help if you tied this bit together explicitly with concrete packs and extensions on tuple types, because it would feel less magical if it had a signature we could write in the language:

extension <Element...> (Element...) {
  var element: Element... { /* it's okay for this implementation to be magic */ }
}

That might also make the behavior of .element on a tuple that contains an element labeled "element" clearer, because we'll need some name-lookup rule deals with extensions on labeled tuple types in the general case.

Also, I was a little surprised that this section doesn't contain an example of forwarding, because I think that's really the big win from getting a pack from a tuple:

struct CapturedArguments<Result, Parameters...> {
  var arguments: (Parameters...)

  func evaluate(with function: (Parameters...) -> Result) -> Result {
    return function(arguments.element...)
  }
}

Forwarding is mentioned earlier, but only in the "this is why packs and tuples are different" section as a reason for making them different.

Now, the moment I see forwarding, scope creep sets in and I would like to also solve the variadic forwarding problem. Can I have .element on an array, for example?

func f(_ args: Int...) { }
func g(_ args: Int...) {
  f(args.element...)  // could this make sense?
}

It's in a sense very different, because the length of the array is a run-time value, so you have a concrete type [Int] that would need to be expanded into a homogeneous parameter pack of runtime-computed length. But I think we can express that notion of a "homogeneous parameter pack of runtime-computed length" already through the generics system:

func gPrime<T...>(_ args: T...) where T == Int { 
  f(args.element...) // okay, I think? expand the homogeneous pack and capture results into array
}

Allowing an array (or any Sequence?) to be turned into a homogeneous parameter pack has a similar feel to implicit opening of existentials, because it takes a run-time-computed value (length for arrays vs. dynamic type for existentials) and turns it into something more static (# of arguments in a pack vs. generic argument of the dynamic type of the existential). For example:

func printAll<T...>(_ args: T...) { }

func printArray<Element>(_ array: [Element]) 
  printAll(array.element...) // # of parameters in the pack "T" depends on length of array!
}

Doug

6 Likes