A vision for variadic generics in Swift

Hello, Swift community!

The design and implementation of variadic generics is well underway. Equipping Swift with a set of features to enable variable-length abstraction will be done incrementally over multiple individual language evolution proposals. The first of these proposals has already been pitched as Value and Type Parameter Packs.

A vision for variadic generics in Swift discusses the overarching design for this set of language proposals. This will give you an idea of the bigger picture, how parameter packs provide the foundation for its future directions, and what sorts of use cases are ultimately supported by this design.

I welcome your questions, thoughts, ideas, and other constructive feedback! For editorial feedback, please feel free to leave a code review on the Swift evolution PR.

-Holly

44 Likes

This document is great! I do have one question about this example:

struct List<Element...> {
  let elements: (Element...)
  init(_ element: Element) { elements = (element...) }
}

extension List {
  func firstRemoved<First, Rest...>() -> List<Rest...> where (Element...) == (First, Rest...) {
    let (first, rest) = (value...)
    return List(rest...)
  }
}

let list = List(1, "Hello", true)
let firstRemoved = list.firstRemoved() // 'List("Hello, true)'

What would be the type of List("Hello").firstRemoved()? Is it a type error, or is it something like Never?

A pack type can be empty, so the type would be List<>.

3 Likes

Huh. So how would I do something like func firstRemoved<First, Rest...> where Rest is empty?

1 Like

We need to solve the representation of one-element tuples first, but something like this, where the RHS of the same-type requirement is a one-element tuple containing your single generic parameter:

extension List {
  func removeLastElement<First>() -> List<> where (Element...) == (_: First) {
    return List<>()
  }
}

Just wanted to say I really like using conditional extensions to add operations when using certain packs. This is a very valuable capability that C++ “gets” by letting substitutions fail for unsupported packs and it leads to fairly terrible diagnostics. I didn’t know how we’d do that in Swift at all and I think the solution is very cool.

1 Like

Are there plans to allow for passing different arguments spelled the same for a pack? Here's an example I ran into recently to explain what I mean:

func resolve<T>(_ type: T.Type = T.self) -> T {
    // ...
}

func addItem<T>(_ callback: () -> T) {
    // ...
}

func addItem<TRet, TDep>(_ callback: (TDep) -> TRet) {
    addItem { callback(resolve()) }
}
func addItem<TRet, TDep0, TDep1>(_ callback: (TDep0, TDep1) -> TRet) {
    addItem { callback(resolve(), resolve()) }
}
// ...as many addItem overloads as you need...

Would I be allowed to say, e.g.

func addItem<TRet, TDep...>(_ callback: (TDep...) -> TRet) {
    let thunk: () -> TRet = { callback(resolve()...) }
    // ...
}

or does variadic generics still not solve this problem?

Yeah, I think what you're looking for is this:

func resolve<T>(_ type: T.Type = T.self) -> T { ... }

func addItem<TRet, TDep...>(_ callback: (TDep...) -> TRet) {
    let thunk: () -> TRet = { callback(resolve(TDep.self)...) }
    // ...
}

You need to reference the pack TDep in the repetition pattern resolve(TDep.self), which is fine because resolve can be passed an explicit metatype argument. The pack expansion resolve(TDep.self)... will repeat the call to resolve(), passing in each TDep.self metatype in the substituted pack, which achieves what you're trying to do IIUC.

4 Likes

Great document so far, I'm really looking forward to be see this all shipping one day.

I have two things still:

  1. Regarding the naming scheme. A possibly middle ground could be to only introduce the pack keyword for the generic type annotation, while keeping the ... for the pack expansion.
func foo<T>(_: T...)
foo(1, 2, 3, 4) // homogenous `T`

func bar<pack T>(_: T...)
bar(1, "swift", true) // heterogeneous `T`
  1. Orthogonal pack expansion / projection.

One other fairly common use case is the projection of individual generic type parameters into their own dedicated enum cases with a single associated value.

enum Either2<A, B> {
  case a(A)
  case b(B)
}

enum Either3<A, B, C> {
  case a(A)
  case b(B)
  case c(C)
}

enum Either4<A, B, C, D> {
  case a(A)
  case b(B)
  case c(C)
  case d(D)
}

// etc.

In case of enums this would highly likely require another proposal to permit at least enum cases to start with digits (e.g. case 3d). In the grand scheme of things I think that variadic generics should ultimately cover these use cases as well.

2 Likes

I'm so excited about this!

I found a couple typos, and I have a few deeper questions and concerns:

Typo #1:

I think the function signature was meant to be:

func element<C: Collection<Element>>(in c: C, at index: any Comparable) -> Element
Typo #2:

The function is defined as printPack but referenced as printPacks.

Relevant Initial Question (Parameterized extensions?)

In the section about conforming tuples to protocols I see parameterized extensions discussed as if they already exist/will be shipped alongside variadic generics - are parameterized extensions now an officially planned future direction for Swift? (:partying_face:)

Primary Question (Get rid of tuples?)

Is it a possibility/has it been considered that we "get rid of" tuples, instead causing the current tuple syntax to simply desugar to an official Tuple<Element...> standard library type? This gives us the ability to conform tuples to protocols and give them extension members without needing either to introduce the unfamiliar tuple extension syntax proposed in the document, nor to implement parameterized extensions yet (although I am separately looking forward to that).

A few more thoughts on extensions of tuples

I just called the proposed tuple extension syntax "unfamiliar", but thinking about it a little more I realize in the same way that extension [Int] { } is now valid I think it would be natural to support:

extension (Int, Bool) { }

which under my suggestion would be equivalent to: extension Tuple<Int, Bool> { }
and would desugar to: extension Tuple where (Element...) == (Int, Bool) { }

If/when parameterized extension are eventually implemented then the proposed tuple extension syntax in the document would actually be totally natural and fine.

Primary Concern (Naming conventions)

This example from the document demonstrates the thing I'm concerned about:

extension ChainCollection {
  subscript(position: ChainCollectionIndex) -> Element {
    // Expand the stored tuple into a local variable pack
    let collection = collections...
    
    func element<C: Collection<Element>>(in c: Collection, at index: any Comparable) -> Element {
      guard let index = index as? C.Index else { fatalError() }
      return c[index]
    }
    
    return element(in: collection[position.collectionPosition],
                   at: position.elementPosition)
  }
}

The line:

let collection = collections...

is what worries me. Firstly, I realize now that I'm not clear on the purpose of the "pack expansion operator" here. Further down in the function where we use the new collection value, why are we not able to simply write:

collections[position.collectionPosition]

?

I see in the document that we are able to use Int to subscript a function parameter that is written like this:

func foo <T> (t: T...) {
    print(t[0])
}

so I imagine the answer to why we have to "unpack" collections is related to why we have to write parentheses around the type of collections?

struct ChainCollection<Element, C...> where C: Collection, C.Element == Element {
  var collections: (C...)
}

I'm sure that there are good reasons and clear explanations for me here, which on the one hand is good news, but on the other hand would mean that this let collection = collections... business is here to stay, and that is where my real concern lies. The resulting value collection can be subscripted with an Int just like an Array. While on the one hand I could imagine an argument (an unconvincing one) for choosing singular names for arrays such that when you subscript them they read a bit more naturally:

giveMoney(to: participant[0])

could be argued to read slightly better than:

giveMoney(to: participants[0])

but in other contexts it would be so massively unclear that it's not worth it and therefore I never name my arrays this way:

sendWelcomeEmail(to: participant)

seems to me incontestably worse (to the point of being unacceptable) as compared to:

sendWelcomeEmail(to: participants)

Therefore, it seems to me so far that collection is only named singularly so as to not conflict with the plurally named collections, which feels like a naming conundrum that could cause code bases that use variadic generics to become absolutely littered with this type of naming inconsistency/confusion.

Is it maybe (hopefully?) the case that the nature of the value collection in the example is such that it is exclusively useful for subscripting and is not to be passed around "whole", and therefore we don't have to worry about it ever being used in the totally confusing way I talked about above?

If not, then maybe we could explore similar solutions to how we have resolved this type of problem before (where a distinct name isn't wanted but is forced)? An example is shadowing an optional value with its own name so as to not have to pick a different one.

Thanks for any and all responses/clarifications!

2 Likes

This document looks very promising, I hope, we can get started with variadic generics in Swift ASAP.


Tangential question

Dynamic pack indexing with Int

Accessing tuple elements as a pack

These two features could be used to partially solve the annoying problem with C fixed-size arrays being imported as tuples, correct?

E.g. imagine the following C struct:

typedef struct {
    char someBuffer[10];
} SomeStruct;

IIRC, this gets imported into Swift roughly like this:

struct SomeStruct {
    var someBuffer: (CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar)

    init() { ... }
    init(someBuffer: (CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar, CChar)) { ... }
}

Could we now still index that buffer using Ints?

var someStruct = SomeStruct()
someStruct.someBuffer.element[0] = 1 // Is this possible?

Very cool.

This is my amateur perspective, but I find the singular naming convention for packs to be very jarring. Take the example below:

extension ChainCollection {
  subscript(position: ChainCollectionIndex) -> Element {
    // Expand the stored tuple into a local variable pack
    let collection = collections...
    
    func element<C: Collection<Element>>(in c: Collection, at index: any Comparable) -> Element {
      guard let index = index as? C.Index else { fatalError() }
      return c[index]
    }
    
    return element(in: collection[position.collectionPosition],
                   at: position.elementPosition)
  }
}

It seems so odd, and potentially a barrier to understanding, that the local variable pack is called collection when in fact it’s a pack of collections.

Now I understand, in that example, the plural name is already taken by the stored tuple collections. (Or can a value pack shadow a property name?) But that is just an example of the more general problem of naming multiple variables that hold the same or closely related data in different ways (e.g. if you convert a Set to an Array, you can’t use the same name).

I’m not sure what would be a better convention — collectionPack? Or maybe have pack names start or end with a particular sigil — like collections* — which would also help to mark them out as something different than a variable of a normal Swift type.

1 Like

This is my primary concern too - your idea of the special sigil reminds of the $ with property wrappers. Could we maybe always have an implicitly generated partner property to foo: (T...) which is referenced using a special symbol? (i.e., $foo would be the same as foo..., but I imagine it's not a good idea/not possible to reuse $).

Well, this is because I forgot to update this example when I made this change to replace something I was calling the "value expansion operator" with a way to access tuple elements as a pack directly :woman_facepalming:t2: thank you for pointing this out and my other typos! I'll get those fixed.

I still think it's important to have a discussion about the naming convention. I'm going to add a section about it to the document because this came up in the first parameter packs pitch too, and I realized that I've never written down my justification anywhere outside a forum discussion thread!

The reason is because C is a type pack whereas (C...) is a tuple type with abstract elements. The latter expands the pack into a tuple type. The subscript and pack expansion operator are available on packs, not tuples. It's unfortunate that ChainCollection needs to store a tuple at all, which is why I think we also need stored property packs; this is addressed in the Concrete packs section.

EDIT: I originally wrote this code against the current parameter packs pitch, which does not include stored property packs. I'm going to change it to just use stored property packs since that's included in the full vision.

1 Like

So fantastic to see this all coming together! In particular, the explanation for patterned pack expansions helped me finally get the mechanics of those operations. I have three questions about additional functionality that would be useful:

First, in the implementation for ChainCollection, we'll need to be able to access to the length of a parameter pack, so that we can create an endIndex that forms the upper bound. We can create that operation in code:

func packLength<T...>(_ elements: T...) -> Int {
    var result = 0
    for _ in elements { result += 1 }
    return result
}

...but it would be better to have something that would be used so frequently as an O(1) operation. Is it possible to add a special length or count property to value packs? Alternatively, we could add a function like the one above to the standard library, but with O(1) performance.

Second, I think there will be value in providing a way to write failable patterned pack expansions, for whole-pack operations that should short-circuit in some circumstances. Right now we can write short-circuiting operations that produce a single value (i.e. finding the first value in a value pack that meets some criteria) or whole-pack operations that produce another same-size pack. For something like the variadic ZipSequence, however, we need whole-pack operations that only produce a new pack if the operation succeeds for every value:

struct ZipSequence<Seq...>: Sequence where Seq: Sequence {
    var sequences: (Seq...)

    struct Iterator: IteratorProtocol {
        var iterators: (Seq.Iterator...)     // note: is this syntax okay?

        mutating func next() -> (Seq.Element...)?
            var iteratorPack = iterators...
            let optionalNextElements = iteratorPack.next()...
            for element in optionalNextElements {
                guard element != nil else { return nil }
            }
            return optionalNextElements!...
        }
    }

    func makeIterator() -> Iterator {
        Iterator(iterators: (sequences...).makeIterator()...)
    }
}

Is there a way to simplify that next() method such that it would only require a single pass through the pack? Or does throwing an error from the expanded expression interrupt things? We could provide a next() wrapper that throws on nil.

Third, how is mutation within value packs supported? Are mutating expressions allowed in a patterned pack expansion? How do we use that when a value pack is stored as a tuple, as in the example above (which I think would need to assign iteratorPack back to iterators)? Also, do pack element projections support mutating operations?

// begin contrived example
func stepFirstTowardZero<T...>(_ values: T...) where T: FixedWidthInteger {
    switch values[0] {
    case 0: break
    case ..<0: values[0] += 1
    case 1...: values[0] -= 1
    }
}
3 Likes

Are we required to use the func method<T...>(_ t: T...) syntax in order to support variadic generics? Is this 100% intentional, or are we going to see some desire for syntax sugar like we did in Swift 5.7 for method(_ t: some T)?

I'm just curious if we'd be able to do func method(_ t: some T...), but I believe today that's interpreted as func method<T>(_ t: T...) (variadic parameter of the same type)

The opaque parameters proposal in Swift 5.7 intentionally banned variadic opaque parameters to leave space for us to decide to sugar parameter packs instead of non-pack variadic parameters using this syntax, so that's totally possible. Starting with the angle brackets syntax is intentional though; the verbose spelling is necessary for types and for signatures that reference the type parameter pack multiple times, and we can always sugar it later.

3 Likes

Parameterized extensions of nominal types are something we would like to add at some point. The syntax for tuple conformances can be introduced without the full feature though, the only overlap is parser support.

Good question.

There are a couple of reasons, maybe not entirely convincing. Tuple elements can have labels, whereas type packs cannot carry labels (as currently proposed, at least).

Another reason is that tuples have special behavior in the type checker with implicit conversions between tuple types where element types convert, for example. Of course, you can imagine a language where Tuple is a type in the standard library that still has special type checker behavior (just like Optional does today, for example).

Tuples are also special in other ways, for example an obscure difference between a tuple and a generic struct is that closures can be stored in a tuple at different abstraction levels (calling conventions) whereas a closure stored in a generic struct (struct G<T> { var t: T }) must be wrapped in a thunk that passes all parameters and results indirectly. But again, Optional has the same behavior where SILGen knows how to apply re-abstraction thunks as needed to the optional payload.

Perhaps if we were starting from scratch, we could do away with tuples as a primitive type entirely, but for now having tuples be their own thing still continues to make sense.

There's no real reason this cannot be supported eventually. My thinking is that adding members to arbitrary tuple types is potentially confusing and merits further discussion, and initially we would only allow this exact syntax

extension <T...> (T...): P where T: P {}

that is,

  • the extension must define a conformance of the tuple type to P
  • the conformance must be conditional on each element conforming to P
  • no other requirements are permitted

However, we could relax these restrictions over time if good use-cases are proposed, and then something like

extension (Int, Bool) {}

would desugar to

extension <T...> (T...) where (T...) == (Int, Bool) {}

You could even imagine something like

extension <T..., U> (T...) where (T...) == (Int, U, Bool), U: Equatable {}

with the shorter equivalent form

extension (Int, some Equatable, Bool) {}

But all that extra sugar could probably go in a stand-alone pitch/proposal.

5 Likes

Ah, I missed this during the SE review. Thanks for the reference!

This cleverly avoids involving the type of an empty tuple, but is that always possible?

protocol SomeProto {
    static func foo()
}

extension String : SomeProto {
    static func foo() { print("hello from String.foo()") }
}

struct S<Element...> where Element : SomeProto {
    init(_ elts: (Element...)) { }
    func dropFirst<First, Rest...> where (Element...) == (First, Rest...) { /* elided */ }
    func callFoo<First, Rest...>(_ type: First.Type = First.self) where (Element...) == (First, Rest...) {
        type.foo()
    }
}

let a = S("hello")
print(type(of: a)) // "S<String>"

let b = a.dropFirst()
print(type(of: b)) // "S<>"

b.callFoo() // what implementation of `foo()` is called?