[Pitch] Parameter Packs

All of these esoteric suffixes are a particularly fanciful distraction.

10 Likes

Be careful to consider other keyboard layouts and input devices such as phones too - it may not be so easy for everyone… three periods is readily available though…

10 Likes

I'm experimenting with packs for the first time right now, and found it unexpected that they don't remove labels. I cannot replicate this:

/// Remove the labels from a tuple.
/// - Parameter tuple: A tuple that may have at least one label.
func removeLabels<T0, T1>(_ tuple: (T0, T1)) -> (T0, T1) {
  tuple
}

with

func removeLabels<each Element>(_ element: repeat each Element) -> (repeat each Element) {
  (repeat each element)
}

Not a big deal for me; I only recall needing that specific overload. Just, again, unexpected.

public extension DictionaryProtocol {
  /// - Note: Differs from the initializer in the standard library, which doesn't allow labeled tuple elements.
  ///     This can't support *all* labels, but it does support `(key:value:)` specifically,
  ///     which `Dictionary` and `KeyValuePairs` use for their elements.
  init(uniqueKeysWithValues keysAndValues: some Sequence<Element>) {
    self.init(uniqueKeysWithValues: keysAndValues.lazy.map(removeLabels))
  }
}

extension Dictionary: DictionaryProtocol { }
extension OrderedDictionary: DictionaryProtocol { }

public protocol DictionaryProtocol<Key, Value>: Sequence where Element == (key: Key, value: Value) {
  associatedtype Key
  associatedtype Value

  init(uniqueKeysWithValues: some Sequence<(Key, Value)>)
}

Hi, it seems that the proposal so far only applies parameter packs to expressions. Based that, is it possible to actually write a variadic zip() function? It seems to me that the necessarily to evaluate and return early when hitting nil could not be done nicely in an expression and thus could not be expressed using the current version of parameter packs. Is that right?

1 Like

It was mentioned in the WWDC23 talk “Generalize APIs with parameter packs” that you can early-exit from a pack iteration by throwing an error. Their sample code is:

protocol RequestProtocol {
    associatedtype Input
    associatedtype Output
    func evaluate(_ input: Input) throws -> Output
}

struct Evaluator<each Request: RequestProtocol> {
    var item: (repeat each Request)

    func query(_ input: repeat (each Request).Input) -> (repeat (each Request).Output)? {
        do {
            return (repeat try (each item).evaluate(each input))
        } catch {
            return nil
        }
    }
}
1 Like

I recommend trying a Swift 5.9 development snapshot from Swift.org - Download Swift for the latest parameter pack fixes.

3 Likes

Yeah the code example given for zip in the proposal doesn't even compile:

struct ZipSequence<each S: Sequence>: Sequence {
  typealias Element = (repeat each S.Element)

  let seq: (repeat each S)

  func makeIterator() -> Iterator {
    return Iterator(iter: (repeat (each seq).makeIterator()))
  }

  struct Iterator: IteratorProtocol {
    typealias Element = (repeat each S.Element) // Error: 'each' cannot be applied to non-pack type '(repeat each S)'
                                                // Error: Pack expansion requires that '()' and 'each S' have the same shape

    var iter: (repeat each S.Iterator)

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

Please report compiler issues with parameter packs at Issues · apple/swift · GitHub.

That error is correct; the proposal has a mistake in this example. The tuple type should be (repeat (each S).Element). Code in a proposal is often written by hand before the compiler support has been implemented, and mistakes happen.

6 Likes

Sorry, I misplaced the errors, that actually does not produce an error for me, but the content of makeIterator() does:

struct ZipSequence<each S: Sequence>: Sequence {
  typealias Element = (repeat each S.Element)

  let seq: (repeat each S)

  func makeIterator() -> Iterator {
    return Iterator(iter: (repeat (each seq).makeIterator())) // Error: 'each' cannot be applied to non-pack type '(repeat each S)'
                                                              // Error: Pack expansion requires that '()' and 'each S' have the same shape
  }

  struct Iterator: IteratorProtocol {
    typealias Element = (repeat each S.Element)

    var iter: (repeat each S.Iterator)

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

The parentheses don't change anything for the type alias, the compiler accepts it regardless.

I believe there is a release note for this in the Xcode Beta 1 notes, but this is the result of an earlier implementation of SE-0399 using a different syntax. I believe in Xcode Beta 1, this should be spelled:

    return Iterator(iter: (repeat (each seq.element).makeIterator()))

This issue should also be fixed in the latest Swift 5.9 development snapshots.

EDIT: This is indeed release noted as a known issue in the Xcode 15 release notes: Xcode 15 Release Notes | Apple Developer Documentation

  • Workaround: Expanding tuples containing the elements of a parameter pack as described by SE-0399 must be spelled with .element: func expand<each Element>(tuple: (repeat each Element)) { _ = repeat each tuple.element }
1 Like

Alright thanks for that, this is one error solved, next issue that I'm trying to solve:

  struct Iterator: IteratorProtocol {
    typealias Element = (repeat (each S).Element)

    var iter: (repeat each S.Iterator)

    mutating func next() -> Element? {
        do {
            return (repeat try (each iter.element).throwingNext())  // Error: Cannot use mutating member on immutable value of type 'τ_1_0.Iterator'
        } catch {
            return nil
        }
    }
  }

Unfortunately we don’t support pack expansion expressions in lvalue context (on the left hand side of assignment, or as an argument to an inout parameter) yet.

This means that you can’t actually write a ZipIterator today. Parameter packs will support lvalues, for loops and other features in the fullness of time, which will make them more useful for these kinds of algorithms.

4 Likes

Sooo, no zip? :frowning:

Not right now, but we are actively working to fix compiler issues with parameter packs, so please file issues you run into at Issues · apple/swift · GitHub.

1 Like

Sounds good.

Parameter packs are quite a massive feature, so understandably they're being introduced in stages, and the current form makes sense as a first step to me. I can think of plenty of use-cases, and in particular they're great for libraries that make use of result builders such as swift-parsing, SwiftUI and SwiftCrossUI. I've updated SwiftCrossUI to use result builders instead of hundreds of lines of boilerplate code (generated with gyb), and it's so much more expressive. Ugly several-hundred line files have become 10 lines. Unfortunately there was a compiler bug that stopped it from compiling, but last time I tested it out was before WWDC, so maybe that bug has been addressed by now. Either way, bugs shouldn't be seen as pitfalls of the feature, it's still pretty experimental from what I can see; Apple just likes rushing things into an Xcode beta in time for WWDC.

7 Likes

While playing around with parameter packs, I discovered a tiny flaw in the current design of the feature and how it plays together with the fact that tuples of one element are the element type itself.
Namely, I was implementing an isEmpty property for the Tuple struct I was playing around with.

It looks like this:

struct Tuple<each Element> {
    private var element: (repeat each Element)

    static var isEmpty: Bool {
        (repeat each Element).self == Void.self
    }

    init(_ element: repeat each Element) {
        self.element = (repeat each element)
    }
}

This does almost work, but unfortunately it returns true not only when Element := {}, but also when Element := {Void}:

print(Tuple< >.isEmpty) // true
print(Tuple<Int>.isEmpty) // false
print(Tuple<Void>.isEmpty) // true :(
print(Tuple<String, String>.isEmpty) // false
print(Tuple<Void, Void>.isEmpty) // false
Note

Notice that I had to write Tuple< > instead of Tuple<>, because <> is parsed as an operator right now. I hope that it's possible to special-case this in the parser.

I don't know if there is anything that can be done here. IMHO, it should be possible to find out unambiguously if a parameter pack is empty or not.

Another note

BTW, I was quite surprised that parameter packs were announced at WWDC already. AFAICT, there is still a lot to be done before even the basic functionality works properly (e.g. given the Tuple struct above, I haven't yet found a way to call the initializer w/o segfaulting using the latest development snapshot^^). Of course, I hope that this feature will be ready soon (I'm super hyped to finally use it) but with the amount of stuff that is still broken, I kinda doubt that it will be too soon.

2 Likes

I think the right approach is to introduce an API that will return the dynamic length of a pack. Then, you can just check if the dynamic length is 0. You can compute it yourself today:

struct Tuple<each Element> {
    private var element: (repeat each Element)

    static var isEmpty: Bool {
       var count = 0
       repeat ((each Element).self, count += 1).1 // the '.1' is just to make the pattern type Void overall
       return count == 0
    }

    init(_ element: repeat each Element) {
        self.element = (repeat each element)
    }
}

print(Tuple< >.isEmpty) // true
print(Tuple<Int>.isEmpty) // false
print(Tuple<Void>.isEmpty) // false
print(Tuple<String, String>.isEmpty) // false
print(Tuple<Void, Void>.isEmpty) // false

But obviously it would be nicer if there were a better, constant-time way to grab the pack length :slightly_smiling_face:

2 Likes

Could there be a special protocol like Pack type every pack would implicitly conform to? Something along the lines of Actor for actor types.

Of course there is a solution for my problem that only slightly abuses syntax :D

Yeah, especially because it's not always possible to build this kind of counter:

struct Foo<each T> {
}

// Add members to `Foo` if `T` is empty. Maybe something like this?
extension Foo where (each T).isEmpty {
    // special members go here
}