Variadic Generics

Hello Swift Evolution,

I'm pleased to bring you a refined copy of the variadic generics proposal, which was first pitched in this thread.

The proposal text is available inline and online.

I look forward to your feedback and questions.


Variadic Generics

Introduction

Functions that take a variable number of arguments have proven to be an effective tool in Swift. Up to now, these functions could only accept a single type of argument. Authors have worked around this restriction by using less specific types like Any or by creating lots of overloaded boilerplate code at each argument arity.

Generic programming provides an effective answer to these challenges. This proposal seeks to unify Swift’s generic programming capabilities with its variable-arity programming features by adding variadic generics to Swift.

Motivation

Functions and aggregate types in Swift currently require a fixed number of generic arguments. To support functions that need to take an arbitrary number of arguments of any type, Swift currently requires erasing all the types involved. A prime example of this kind of function is print, which is declared with the following signature in the Swift standard library:

`func print(_ items: Any..., separator: String = " ", terminator: String = "\n")`

The print function uses a number of runtime tricks to inspect the type of each argument in the pack of existential items and dispatch to recover the appropriate description. But we also have many situations where the user wants to write a type-safe, generic, variadic function where the types of the arguments cannot be erased in this manner.

SwiftUI’s primary result builder is a type called ViewBuilder. Today, ViewBuilder is artificially constrained to accept between 0 and 10 sub- View elements of differing types. What’s more, for each given arity, a fresh declaration of buildBlock must be given, which leads to an explosion in the amount of boilerplate necessary to build the strongly-typed API that such a library desires. What would be ideal is if the Swift language provided a built-in way to express variadic generic functions. For our SwiftUI example above, we would like to write one and only one buildBlock function.

extension ViewBuilder {
    public static func buildBlock<V...>(views: V...) -> (V...) 
      where V: View
    {
        return views
    }
}

Finally, tuples have always held a special place in the Swift language, but working with arbitrary tuples remains a challenge today. In particular, there is no way to extend tuples, and so clients like the Swift Standard Library must take a similarly boilerplate-heavy approach and define special overloads at each arity for the comparison operators. There, the Standard Library chooses to artificially limit its overload set to tuples of length between 2 and 7, with each additional overload placing ever more strain on the type checker. Of particular note: This proposal lays the ground work for non-nominal conformances, but syntax for such conformances are out of scope.

Proposed solution

We propose the addition of generic parameter packs to Swift’s generics. These packs enable types and functions to declare that they accept a variable number and type of argument:

// 'max' accepts a pack of 0 or more Comparable 'T's
func max<T...>(_ xs: T...) -> T? where T: Comparable { /**/ }

// Tuple<T, U, V, ...> accepts a pack of element types
struct Tuple<Elements...> { /**/ }

To support the ability to store and return parameter packs, we propose the addition of pack expansion types to Swift.

// The return type of this function '(T...)' is a pack expansion. It lets you
// gather variadic arguments together into a generic tuple-like value 
//
// let (s, i, d) = tuple("Hi", 42, 3.14) // : (String, Int, Double)
// let s = tuple("Hi") // : String
// _ = tuple() // ()
func tuple<T...>(_ xs: T...) -> (T...) { return xs } 

// You can have stored properties with a pack expansion as a type
struct Tuple<Elements...> { 
  var elements: (Elements...)
  init(elements: Elements...) { self.elements = elements }
}

Detailed design

Variadic Generic Parameters

Variadic generic parameter declarations are formed from “plain” generic parameter declarations plus the addition of trailing triple dots as in func tuple<Elements...>. Similarly, a type may declare a variadic generic parameter struct Tuple<T...>. A function may have any number of variadic generic parameters. Types, by contrast, support only one variadic generic parameter because of a lack of delimiter when instantiating them

func double<Ts..., Us...>(ts: Ts..., us: Us...) { /**/ } // ok
struct Broken<T..., U...> { /**/ }
typealias Bar = Broken<String, Int, Void> // Which types are bound to T... and U...?

Generic Parameter Packs

Swift already has a syntax for variadic parameters in functions: xs: T..., as well as a number of rules around variadic applies that are already semantically well-formed. We choose to overload the syntax of variadic parameters to inherit this behavior, but also to show by extension that the semantics of variadic generic parameters are no different than those of monomorphic variadic parameters.

There is one peculiarity of this choice, though. Today, the interface type of xs: T... is sugar that produces an array type. This allows the user to reason about variadic parameters as a high-level container type. Parameters of variadic generic type will similarly implicitly construct a container type. However, instead of an array type, variadic generic parameters will be a new kind of container: packs. One can read the parameter in the following function as “elements is a pack of Ts”

func tuple<Ts...>(_ elements: Ts...) {}

The type parameter Ts is the “pattern type” of the constituent elements of the pack. It need not solely be a reference to a variadic generic parameter. The following demonstrates more advanced usages of pattern types in packs:

// This function: 
// - Takes a variadic sequence of (possibly-distinct) arrays of optional values
// - Removes the optional values from each one
// - Returns a pack with arrays sans `nil` elements.
func flattenAll<Ts...>(_ seqs: [Ts?]...) -> ([Ts]...) { 
  return seqs.map { $0.compactMap { $0 } }
}

Pack Expansions

In order to support parameter packs in the return position of functions and as stored properties, we will need to introduce new syntax. At the type level, a parameter pack is nothing more than a tuple-like value with its elements and arity presented abstractly. Swift already has a natural space in its tuple syntax for just this operation: (T...) - read as the “pack expansion of T”. As the name implies, the type of the elements of (T...) are precisely the types of the elements of the pack used to construct T... when the function is called. Put another way, (T...) “unfolds” to become the (tuple) type of the elements provided to its parent variadic generic function. Returning to the variadic identity function from before:

func tuple<T...>(_ xs: T...) -> (T...) {
  return xs
}

tuple() // xs: Void
tuple(42) // xs: (Int) = Int
tuple(42, "Hello") // xs: (Int, String)

This is an extremely powerful typing rule, as it allows for the output of a function to observe the arity of its inputs - a form of dependent typing.

One other thing to observe about this example is that we do not require any special syntax to convert an input parameter pack to an output expansion. Because we take the view that the pack itself is a scalar object, there is no need to present a user-visible syntax to “splat” its elements. We simply forward the pack on, or transform it - by analogy with existing variadic parameters.

Iteration

Going further, we can extend existing language constructs to support values with variadic types to enable a much more fluid syntax. Consider a slightly modified typesafe variadic version of print:

func printTypes<Args...>(_ items: Args...) {
  for (item: Args) in items {
    print(type(of: item), item)
  }
}

This proposed syntax uses Swift’s existing for-in loop construct to enable iteration over a pack. This shows how we can manipulate individual elements of the parameter pack one-by-one and can be accomplished with a builtin conformance of packs to Sequence.

Simultaneous operations on every element of a pack are also extremely important to have as language primitives. To support this, pack types will come with a “magic map” operation that functions identically to its cousin for Swift Sequences - only this time it transforms the elements of a pack. A hypothetical internalization of this function is

extension<T...> (T...) {
  public func map<U...>(_ transform: (T) throws -> U) rethrows -> (U...) { /**/ }
}

Forwarding

Implementations of variadics inevitably run into the problem of how to send one pack of arguments to another function that accepts a parameter pack. This is usually achieved via an operation called “splat” or “explode”, and one can think of it as taking a pack, breaking it down into its component parts, then calling the function. This usually involves special syntax like * in Ruby and Python, or C++‘s trailing ... syntax (read as “pack out”). Since this proposal takes the tact that packs are values in their own right, we see no need for additional syntax. Therefore, we propose the removal of the restriction on passing variadic arguments to variadic parameters.

func forward<T...>(_ xs: T...) { return onwards(xs) }
func onwards<T...>(_ xs: T...) { /**/ }

A variadic argument is said to “saturate” a variadic parameter which means you cannot provide additional arguments before or after forwarding a variadic argument:

func forward<T...>(_ xs: T...) { 
  onwards(xs, "oops") // error: Too many arguments to 'onwards'!
  onwards("oops", xs) // error: Too many arguments to 'onwards'!
}
func onwards<T...>(_ xs: T...) { /**/ }

As a historical note, Swift already has the ability to forward (plain) variadic parameters, the user just cannot spell it. Internally, the implementation of compiler-provided overrides of variadic initializers (among other synthesized language elements) produces a variadic-to-variadic conversion on your behalf.

At the type level, it is also useful to be able to forward a pack of arguments to another pack of arguments. But it is also useful to forward the pack of elements as a single tuple type. To support both of these behaviors, a pack expansion type T... appearing bound as a generic parameter will act to forward any expanded elements.

struct Forward<T...> {}
typealias Onwards<T...> = Forward<T...>

// Onwards<Int, String, Void> = Forward<Int, String, Void>

While a pack expansion type appearing parenthesized in this same position will act to bind a single tuple type after it has been expanded

struct Forward<T...> {}
typealias Onwards<T...> = Forward<(T...)>

// Onwards<Int, String, Void> = Forward<(Int, String, Void)>

All Together Now

Using all of these concepts, we present a generalization of the standard library’s zip function to an arbitrary number and kind of sequences:

public func zip<Sequences...>(_ seqs: Sequences...) -> ZipSequence<Sequences...> 
  where Sequences: Sequence
{
    return ZipSequence(seqs)
}

public struct ZipSequence<Sequences...>: Sequence
  where Sequences: Sequence 
{

  private let seqs: (Sequences...)
  
  public init(_ seqs: Sequences...) {
      self.seqs = seqs  
  }
  
  public func makeIterator() -> Self.Iterator {
    return Self.Iterator(self.sequences)
  }
  
  public struct Iterator : IteratorProtocol {
    private enum Stop { case iterating }
    
    public typealias Element = (Sequences.Element...)
    
    private var reachedEnd = false
    var iterators: (Sequences.Iterator...)

    init(_ iterators: Sequences...) {
      self.iterators = iterators.map { $0.makeIterator() }
    }

    public mutating func next() -> Element? {
      if reachedEnd { return nil }

      guard 
        let values = try? iterators.map({ 
          guard let next = $0.next() else { throw Stop.iterating } 
          return next
        })
      else {
        reachedEnd = true
        return nil
      }
      return values
    }
  }
}

Semantic Constraints

  • A pack expansion that does not involve a variadic generic parameter is ill-formed outside the body of a function or closure
typealias Nope = (Int...)
typealias Nope<Again> = (Again...)

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

struct Nope {
  var xs: (Int...)
}

func ok<T...>(_ xs: T...) {
  let zeroes: (Int...) = xs.map { 0 }
  // There's not much you can do with this value...
  
  struct NestedNope {
    var zeroes: (Int...) // Nope!
  }
}
  • Pack expansion types may not appear in associated type requirements for protocols. They may, however, appear in type witnesses themselves
protocol Nope { associatedtype (Ts...) }
  • Parameters of pack type cannot be inout, in keeping with existing language restrictions
func foo<T...>(_ xs: inout T...) {} // 'inout' must not be used on variadic parameters

// This doesn't work either!
func foo<T>(_ xs: inout T...) {} // 'inout' must not be used on variadic parameters`
  • Pack expansion types may not appear in inheritance clauses
protocol Nope : (T...) {}
class Nope<Again...> : (Again...) {}
  • A pack expansion type may not appear in the position that would otherwise bind a non-variadic generic parameter
struct Singleton<T> {}
typealias PackOf<Ts...> = Singleton<Ts...> // Oops!
  • The arity of an argument pack must remain consistent when variadic generic types are mentioned in multiple parameters
// 'first' mentions T... and U..., so it must have the
// same arity as 'second' which mentions 'T...' and
// the same arity as 'third' which mentions 'U...'
func foo<T..., U...>(
  first: [T: U]...,
  second: T...,
  third: U...
) where T: Hashable

foo(first: [String:Void](), second: "K", third: ()) // ok
foo(first: [String:Void](), second: "K", "L", third: ()) // nope!
  • Equality constraints between variadic generic parameters must similarly only involve other variadic generic parameters.
func foo<T..., U>() where T == U // nope!
  • Equality constraints that would concretize a pack expansion type are banned.
func foo<T..., U>() where (T...) == (Int, String, Void) // nope! 

Grammatical Ambiguities

The use of ... in parameter lists to introduce a generic variadic parameter introduced a grammatical ambiguity with the use of ... to indicate a (non-generic) variadic parameter. The ambiguity can arise when forming a pack expansion type. For example:

struct X<U...> { }

struct Ambiguity<T...> {
  struct Inner<U...> {
    typealias A = X<(T...) -> U...>
  }
}

Here, the ... within the function type (T...) -> U could mean one of two things:

  • The ... defines a (non-generic) variadic parameter, so for each element Ti in the parameter pack, the function type has a single (non-generic) variadic parameter of type Ti, i.e., (Ti...) -> U. So, Ambiguity<String, Character>.Inner<Float, Double>.A would be equivalent to X<(String...) -> Float, (Character...) -> Double>.
  • The ... expands the parameter pack T into individual parameters for the function type, and no variadic parameters remain after expansion. Only U is expanded by the outer .... Therefore, Ambiguity<String, Character>.Inner<Float, Double>.A would be equivalent to X<(String, Character) -> Float, (String, Character) -> Double>.

If we take the first meaning, then it is hard to expand a parameter pack into separate function parameter types, which is necessary when accepting a function of arbitrary arity, e.g.,

func forwardWithABang<T..., R>(_ args: T?..., body: (T...) -> R) -> R {
  body(args.map { $0! })
}

Therefore, we propose that the ... in a function type be interpreted as a pack expansion type when the pattern of that type (e.g., the T in ...) involves a parameter pack that has not already been expanded. This corresponds with the second meaning above. It is source-compatible because the “pattern” for existing code will never contain a parameter pack.

With this scheme, it is still possible to write code that produces the first meaning, by abstracting the creation of the function type into a typealias that does not involve any parameter packs:

struct X<U...> { }

struct AmbiguityWithFirstMeaning<T...> {
  struct Inner<U...> {
    typealias VariadicFn<V, R> = (V...) -> R
    typealias A = X<VariadicFn<T, U>...>
  }
}

Note that this ambiguity resolution rule relies on the ability to determine which names within a type refer to parameter packs. Within this proposal, only generic parameters can be parameter packs and occur within a function type, so normal (unqualified) name lookup can be used to perform disambiguation fairly early. However, there are a number of potential extensions that would make this ambiguity resolution harder. For example, if associated types could be parameter packs, then one would have to reason about member type references (e.g., A.P) as potentially being parameter packs.

Source compatibility

The majority of the elements of this proposal are additive. However, the use of triple dots ... in the type grammar has the potential to conflict with the existing parsing of user-defined operators. Take this stream of tokens:

T...>foo

This has the potential to be parsed as an operator ...> applied to two arguments as in

(T) ...> (foo)

or as a fragment of a generic parameter as in

/*HiddenType<*/T...> foo

To break this ambiguity, in function declarations and type expressions we require that the dots ... always bind to their preceding generic parameter, if any.

We strongly suspect there are little to no libraries taking advantage of this kind of operator overloading, but we wish to call it out as it creates a new reserved class of expression in the grammar.

Effect on ABI stability

This proposal is additive, and has no affect on the existing ABI of libraries.

Effect on API resilience

This proposal is additive, and has no affect on the existing API of libraries.

Alternatives considered

There are a number of alternative formulations of variadics that have been implemented in many different languages. The approach detailed here is particularly inspired by the work of Felleisen, Strickland, and Tobin-Hochstadt who presented the bones of the inference algorithm needed to implement the type system extensions.

Explicit Syntax for Pack Expansion in Expressions

“Magic map” is a way to sidestep this particular requirement of C++‘s implementation of variadics. There, ... in postfix position in an expression directs the compiler to syntactically expand any packs in the preceding expression and optionally “fold” it around an operator. Explicit pack expansion syntax has a number of upsides, but it also complicates the forwarding story presented above. Most glaringly, Swift already has a meaning for postfix ... in expression position with one-sided ranges.

Alternative Syntax for Variadic Generic Parameters/Expansions

We chose ... by analogy with Swift’s existing syntax for variadic parameters. However, there’s no reason we cannot pick a different syntax, especially one that does not conflict with one-sided ranges. Suggestions from the first pitch thread include: T*, *T, variadic T, pack T, T[], and many more.

Future directions

Non-Nominal Conformances

We do not explicitly propose a syntax for non-nominal extensions and protocol conformances involving pack types. In concert with parameterized extensions, however, there is a natural syntax:

extension<T...> (T...) {
  public var count: Int { /**/ }
}

extension<T...> (T...): Equatable where T: Equatable {
  public static func == (lhs: Self, rhs: Self) -> Bool { 
    for (l, r) in zip(lhs, rhs) {
      guard l == r else { return false }
    }
    return true
  }
}

extension<T...> (T...): Comparable where T: Comparable {
  public static func < (lhs: Self, rhs: Self) -> Bool { 
    guard lhs.count > 0 else { return true } 
    
    for (l, r) in zip(lhs, rhs) {
      guard l < r else { return false }
    }
    return true
  }
}
import SwiftUI

extension <T...> (T...): View where T: View {
    var body: some View { /**/ }
}

Additional Structural Pack Operations

A natural extension to this feature set is the ability to concatenate and destructure compound pack expansion types and their corresponding values. To support the former, a magical append function could be added that looks as follows:

extension<T...> (T...) {
  public func append<U...>(_ xs: U...) -> (T..., U...) { /*poof!*/ }
}

To support the latter, the ability to abstract over multiple values in patterns is needed:

let (first, remaining...) = ("A", "B", "C").append(1, 2, 3)
// first: String, last: (String, String, Int, Int, Int)

It would also make sense to support the mixing of pack expansions with normal tuple element types as in

func atLeastTwo<T, U, V...>(_ x: T, _ y: U, v: V...) -> (T, U, V...) {
  return (x, y).append(v)
}

let (x, y, rest...) = atLeastTwo("Hello", 42) // rest: ()

Explicit Arity Constraints

We could allow users to reason explicitly about the arity of packs. In particular, they could present inequalities to the type checker and those could, in turn, restrict the types of packs allowed at a particular apply. A purely illustrative example is:

extension <T...> (T...) where (T...).count >= 4 {}
68 Likes

Could you clarify why the pack of VG types can't be inout?

Furthermore, which means that all future consuming/ref/inout type values couldn't be passed to VG parameters?

This looks great!

First of all, thank you for pushing further on this! Everything seems well thought out and I find the new terminology very clear.

Is this a pragmatic limitation or a design decision? My gut feeling is that protocols having associated packs might be a useful future direction, specifically for providing an improved form of Codable.

5 Likes

Oh, and what would a pack’s Element type be?

One additional clarification request, the example above seems to be the only one which has a non-trivial pack expansion. Could you elaborate a bit on what the underlying mechanism here is, is it just "any type expression which has the pack parameter in it"? For examples, users might want to define their own functions with signatures like the following (admittedly contrived) examples.

func getTypes<T...>(_ pack: T...) -> T.Type... { … }
func map<T...>(_ pack: T...) -> T.SomeAssociatedType... where T: SomeProtocol { … }
func wrap<T...>(_ pack: T...) -> SomeWrapperType<T>... { … }

Also, would it be possible to express relationships between packs?

struct Foo<A, B> 
where 
  A: SomeProtocol, B: SomeProtocol 
  A.SomeAssociatedType == B.SomeAssociatedType
{
}
func merge(a: A..., b: B...) -> Foo<A, B>...
where
  /// A and B satisfy Foo's requirements
{ … }
1 Like

A small comment on the proposal text:

It seems clear to me from the detailed description that the only viable "spelling" for generic type placeholder symbols is the singular form, not the plural. So, T..., not Ts..., or Arg..., not Args.... "Pluralizing" the symbol works especially badly in this:

  for (item: Args) in items {

For that reason, I'd suggest changing the proposal text to use the singular forms everywhere.

On the other hand, using a plural form in parameter/keyword symbols — items: — seems correct. However, I find it takes work to grasp that xs is intended to be the plural of x. It bothered me every time, not just the first time I encountered it.

I'd suggest changing the proposal text to use an actual word instead of xs:, such as values:.

These textual changes wouldn't affect what's being proposed, but would I think make it significantly easier to read.

15 Likes

Thanks for your continued work on this, @codafi!

I remain somewhat averse to the ... spelling for reasons mentioned in the previous thread. Most concerning to me is:

IMO, although the ... syntax is nice and terse, the potential breakdown of local reasoning ability is a good reason to prefer something else.


I don't know where exactly the Swift API design guidelines sit with regard to Swift evolution, but I think that as part of the variadic generics design we should settle on some conventions which could eventually be subsumed as part of the API guidelines. In particular, I am still bothered by the interchangeable use of plural and singular terminology for generic parameter packs. It looks like this proposal has mostly settled on plural terminology (though we still have some T, U usages rather than Ts and Us), but IMO that becomes a bit problematic given:

because it means that the parameter name is used in contexts that refer to the aggregate pack (plural) as well as the individual constituent pack types. I think this makes for some confusing signatures such as:

Here, Sequences: Sequence is really saying "each constituent type of Sequences is itself a Sequence, but IMO a perfectly plausible reading of this is "the Sequences pack is a Sequence itself." This particular example is made even more confusing by the fact that the erroneous interpretation is in fact true, since the pack as a whole does conform to Sequence.

This strangeness can also be seen in:

The type of item is really a single 'arg' from the Args... pack, but the use of the plural terminology reads as if item is itself some sort of aggregate type.

I'd really like it if we could have a design which allows for explicit differentiation between the singular usage and the aggregate usage of the variadic parameter names. In the previous pitch I came up with a somewhat verbose forEach syntax that would allow for the explicit introduction of the 'singular' name of a variadic parameter:

but I'd also be fine with some sort of syntax that indicates "we're really talking about an arbitrary element of this pack, maybe something like:

public struct ZipSequence<Sequences...>: Sequence
  where Sequences[_]: Sequence 
{

ETA: @QuinceyMorris's suggestion of always preferring the singular spelling for variadic generic parameters is also compelling to me, and wouldn't require the invention of new syntax to refer to a singular element. The bare name (e.g., Arg) would always refer to a singular element, and the aggregate would always be spelled with the trailing ... (e.g., Arg...). That also agrees with the existing usage of ... for variadic parameters, for which we write the singular type name and use ... to 'pluralize' it, e.g., Int....

Regardless of which direction we choose, though, I think the proposal should take an opinionated stance on the 'right' way to design variadic generic signatures.


I'm also still wondering if we really need to introduce the new "generic parameter packs" to the language:

If we were to 'superpower' tuples, could we get away with not introducing an entirely new concept to the language? Is there a fundamental expressivity issue with tuples that prevents us from realizing everything we would want from variadic parameters?


Regarding the "only one variadic parameter in a generic type" rule, is this a restriction we want to lift eventually? If so, could we allow such types to be defined and rely on generic parameter inference to fill in each pack appropriately?

11 Likes

A couple more questions:

  1. What is the expected syntax around empty parameter packs for functions. E.g.:

    func foo<T..., U...>(vals: T..., vals vals2: U...)
    
    // impossible to call with `(T...) == ()`?
    // should implicitly empty packs be allowed?
    foo(vals: 1, 2, 3) // what are `T...` and `U...` here?
    
    // do we need a way to specify explicitly empty packs?
    foo(vals:, vals: 1, 2, 3)
    

    I suppose the answer could just be "the author of foo did a bad thing by repeating the argument label", but in that case should we just disallow such functions from being formed (e.g., "variadic parameter packs in function signatures must all have distinct labels" or something?)

  2. Similarly, what about empty parameter packs for types?

    struct Foo<T...> {
      init(ts: T...) {}
    }
    Foo<>() // ok?
    Foo() // implicitly 'Foo<>'?
    
    
  3. How do we want properties of generic parameter pack tuples to interact with the synthesized initializer? E.g.,

    struct Foo<T...> {
      vals: (T...)
    }
    Foo(vals: 1, 2, 3) // ok? or do I have to...
    Foo(vals: (1, 2, 3))
    
4 Likes

Please could you add syntax highlighting to the pull request.


Would a type-safe print only avoid boxing the items as Any, but still need runtime checks for the Custom[Debug]StringConvertible and TextOutputStreamable conformances?


How would this (and the examples in Future directions) be able to compare across different types in the pack? The < and == operator functions have Self operands.


The semi-colon delimiter (Broken<String; Int, Void>) is probably available.

7 Likes

I am assuming that something like this will be allowed:


struct X <U…> {
    private let somePack: (U…)
    init ( someInput: (U…) ) { 
       somePack = someInput // is this the forwarding ?
    }
}

Are packs just heterogeneous tuples?

I am having trouble understanding how packs ( which can be heterogeneous ) are Iterable ( for loop into them).

In the above example the Args’s type is potentially different. In todays code that would have to be Any.

1 Like

The type would be different for each argument in the pack, but each type would be known statically at compile time. I imagine it would be equivalent to writing out...

do {
    // obviously, there's no proposed syntax for getting the nth element of a pack
    // so this is just for exposition.
    let item = pack.0
    print(type(of: item), item)
}
do {
    let item = pack.1
    print(type(of: item), item)
}
// etc.

Until you've covered each item in the pack. Although the compiler would potentially do something more efficient than that.

This is great!

I’ve been looking forward to variadic generics for a long time, and I like the direction you’ve gone here.

I’m sure I’ll have more thoughts and questions after I’ve digested the proposal a bit, but for now I just want to ask about one of the listed restrictions:

I think this could be useful, especially for constrained extensions:

struct Foo<T...> {
  // something
}

extension Foo where (T...) == (Int, String, Void) {
  // some particular things
}
13 Likes

I was just thinking about this, and boom yesterday someone made a proposal. The universe is funny like that!

Specifically, what I would like to do is something like this:

func foo<Root..., Value...>(keyPaths: KeyPath<Root, Value>...) { 
    func process<Root>(_ keyPath: KeyPath<Root, Int>) { ... }
    func process<Root>(_ keyPath: KeyPath<Root, String>) { ... }
    /// Gets called if value isn't `Int` or `String`.
    func process<Root, Value>(_ keyPath: KeyPath<Root, Value>) { ... } 
    for keyPath in keyPaths {
        process(keyPath)
    }
}

Can we update your proposal such that this would be handled, or would it be non-feasible?

You've discussed "packs" at length but not given an example of how a pack is declared in Swift. What is it exactly?

E.g.

struct Pack: Packable { ??? what goes in here ??? }

Questions about packs:

  1. How is pack defined exactly?
  2. How is a pack different from a collection exactly?
  3. How can you programmatically access the individual members of a pack?
  4. How does a pack interact with reflection and debugger introspection?
  5. Why is it called a "pack" (a word that usually refers to a group of things of identical type to one another, like a pack of cards or a pack of wolves) as opposed to using a word that better reflects the fact that it can be a group of completely diverse types, such as "medley"?
  6. If "pack" refers to the group of generic types in the generic params, then what refers to the type of the collection of various-typed objects in the function parameters...? (Are these also a pack, or still a collection?)
  7. how do you deal with capture groups and packs?

E.g.:

func foo<T..., U...>(_ t: T..., u: U...) {
    let myClosure: () -> Void = { [t, u] in
        ...
    }
}
  1. What types are captured t and u inside the closure? I.e. is t a "pack" or a "collection"?
  2. Is t a copy of the original pack t or is it the same instance, just retained, because packs are reference types?
  3. What happens if you print(type(of: t))?
  4. What if some T's and U's are reference types and others are not... ?
  5. Can weak modifier be applied to the capture group argument in a way that applies only to objects among the various things in t or u?
  6. Does this proposal require any dynamic behaviors or can it rely purely on static behaviors as current generics do?

Please disregard if you've already discussed elsewhere how these "packs" deal with memory concerns when they contain some value types and some reference types.

How about just allow variadic generic parameters to have parameter labels, e.g.:

struct Broken<A: T..., B: U...>
typealias Bar = Broken<A: String, B: Int, Void> 

... in which case, the type of A is something like, PackWrapper<T...> and the type of B is PackWrapper<U...>. Thoughts?

1 Like

First things first, thank you very much for this updated proposal on the subject!

Then, what if we treat "packs" as Collections, with a capital "C"? You can have all the benefits of map & co. (and even more, if we can / want to give them conformances to BidirectionalCollection and / or RandomAccessCollection), and moreover they can be blessed with a little compiler magic so that:

  • in static contexts the compiler knows order and position of all the members, and maybe can allow direct access using (checked?) indexed or dot notation
  • in dynamic / completely generic contexts you can rely on Collection operations to do all the necessary work

In this way you are not forced to immediately convert Variadic Generics to tuples, but can do so if needed, and you need not introduce a "special" map operation.

Speaking of map, I think the operation should take into account mutability: as currently defined you cannot, for example, invoke Iterator.next() because it is a mutating method. However, would we define the parameter to be inout, I'm not sure about map being the correct name (not sure about the relationship between inout and pure functions).

5 Likes

Thanks for actively working on this, @codafi! This is a long-awaited feature.
A couple of comments:

This example requires all Ts in the pack being the same type, as Comparable has Self type requirement, so this is not a case of variadic generics, but a regular generic function with a variadic parameter:

func max<T>(_ xs: T...) -> T? where T: Comparable

The variadic generic format would allow writing max(“Hi”, 42, 3.14, Date()), which technically satisfies the function signature (since all of String, Int, Double, and Date do conform to Comparable), but they’re all of different types, therefore the implementation of the function max is semantically impossible.
Perhaps a better example would be to constraint T… to Numeric instead.


I also find the idea of pack expansions a bit problematic. Apart from the fact that it could produce single element tuples, makes the syntax T... ambiguous for the reader, because if refers to different concepts in each position. In the first position (between the angle brackets), it signals that this generic parameter is something that is not a concrete, single type, but a sequence of those. The second (in the parameter position) is the already existing variadic syntax. While the last tuple-like one refers to a concrete return type of an anonymous sequence of existential types. This mixture can become a source of confusion. I’d prefer if these 3 occasions were written differently, as they stand for different concepts. For example

func tuple<T*>(_ xs: T...) -> pack T // similar to some T and any T
1 Like

I'm happy this is moving forward. I read the current proposal and came away with the following 3 questions:


It sounds like in func buildBlock<V...>(views: V...) -> (V...) where V: View, each V can be an independent type that conforms to View (all views don't need to be the same type). However, right at the start of the Proposed Solution section, we see the following example:

func max<T...>(_ xs: T...) -> T? where T: Comparable

which isn't workable if each T is allowed to be separate since Comparable can only compare Self objects.

It seems to me that max could be currently written as func max<T>(_ xs: T...) -> T? where T: Comparable (except that it would use monomorphic variadic parameters instead of the new parameter packs), so I'm guessing that the first interpretation is right and this example needs to be updated?


How do you explicitly specify the generic parameters of functions with multiple variadic generic parameter packs? In double<Ts..., Us...>, if I want a "function pointer" closure to double that has Ts=(Int, Int) and Us=(Double, Double), how do I spell that? double<Int, Int, Double, Double> is ambiguous for the same reasons that types are.


How does iteration work with protocols with associated types? Is for (item: Args) in items magic, given that item: Args is invalid typing as of the current Swift? (Are there interactions with SE-0335 and SE-0309?)

2 Likes

You cannot do that even with normal generic functions.

If you want to reference a generic function you have to use either as or a type hint, e.g.:

func foo<T>(_ t: T) {}

let foo2 = foo<Int> // Error: Cannot explicitly specialize a generic function

let bar = foo as (Int) -> ()
// or
let baz: (Int) -> () = foo

However, this breaks down as well for functions with multiple generic parameter packs:

func foo<T..., U...>(ts: T..., us: U...) {}

let bar = foo as (Int, Int, Double, Double) -> () // What is T and U supposed to be?
// or
let baz: (Int, Int, Double, Double) -> () = foo // Same problem

So the problem remains that we could not reference such a function under this proposal.

3 Likes

How about just allow variadic generic parameters to have parameter labels, e.g.:

struct Broken<A: T..., B: U...>
typealias Bar = Broken<A: String, B: Int, Void>

... in which case, the type of A is something like, PackWrapper<T...> and the type of B is PackWrapper<U...> . Thoughts?

Why not directly have the ability to (optionally) specify the parameter name in the specialisation (this may also be generalised to all generic parameters), e.g.

struct Broken<T..., U...>
typealias Specialised = Broken<T: Int, String, U: Double, Void, String>
10 Likes