[Pitch] Pack Destructuring & Pack Splitting

Hello Swift community,

This post pitches two complementary features to enhance Swift's variadic generics capabilities, building upon SE-0393 (Parameter Packs) and SE-0408 (Pack Iteration).


Motivation

While variadic generics allow abstraction over arity, Swift currently lacks first-class mechanisms for recursive operations on parameter packs. We cannot easily decompose a pack into its "head" and "tail" components at compile time.

This limitation forces library authors into cumbersome workarounds:

  • Fixed-Arity Overloads: Libraries like Combine (zip) and SwiftUI (TupleView) must provide numerous, near-identical overloads (e.g., Zip2, Zip3, ..., Zip10), leading to boilerplate and arbitrary limits.
  • Type Erasure: Using approaches like [AnyPublisher] sacrifices static type safety and can incur runtime costs.

The goal is to provide a type-safe, zero-overhead way to write recursive algorithms over heterogeneous value packs.


Proposed Solution

I propose two orthogonal, compile-time features:

1. Pack Destructuring in Patterns

Allow extracting the first element and the remaining pack within let or case patterns using repeat each:

// Peel off the first element
let tuple: (Int, String, Bool) = (1, "a", true)
let (first, repeat each rest) = tuple
// first: Int = 1
// rest: (String, Bool)

// Works in switch/if case
switch result {
case .success(let firstValue, repeat each otherValues):
    process(firstValue, repeat each otherValues)
case .failure(let error):
    handleError(error)
}
  • head binds to the first element.
  • tail binds to a new pack of the remaining elements.
  • Exactly one repeat each is allowed per tuple level in the pattern.
  • Compile-time error if the pack is empty when destructuring.

2. Pack Splitting in Calls

Allow a single pack expansion repeat each pack at a call site to implicitly provide arguments for an initial non-pack parameter and a subsequent pack parameter.

// Function expecting head + tail
func process<Head, each Tail>(_ head: Head, _ tail: repeat each Tail) { /* ... */ }

// Given a pack:
let pack: (A, B, C) = (a, b, c)

// Call site splits the pack:
process(repeat each pack)

// Compiler desugars to:
// process(pack.0, (pack.1, pack.2)) // pseudo-code
  • Non-pack parameters are bound first.
  • The repeat each parameter consumes the rest.

Example: Recursive Flatten

These features enable concise recursive functions like tuple flattening:

func flatten<each T>(_ values: repeat each T) -> (repeat each T) {
    // Base case: Empty pack (handled by guard)
    // Recursive step: Destructure + Split
    guard let (first, repeat each rest) = (repeat each values) else {
        return () // Return empty tuple for empty pack
    }
    // Combine head + recursively flattened tail
    return (first, repeat each flatten(rest))
}

let nested = (1, ("two", (3.0, true)))
let flattened = flatten(repeat each nested) // Inferred type: (Int, String, Double, Bool)

Impact Example: Combine zip

Instead of numerous overloads:

// Before: 10+ explicit overloads
func zip<A,B>(_ a: A, _ b: B) -> Zip2<A,B> // ...

We could have a single generic implementation:

// After: Single generic struct
struct Zip<repeat each S: Publisher>: Publisher {
    typealias Output = (repeat (each S).Output)
    let publishers: (repeat each S)

    func receive<Sub: Subscriber>(subscriber: Sub) /* ... */ {
        // Use destructuring to peel off first publisher
        let (first, repeat each rest) = publishers
        // Recursively handle 'rest' (details omitted for brevity)
        // ...
    }
}

This eliminates hundreds of lines of boilerplate and removes arbitrary arity limits.


Compatibility

These features are purely additive at the source level and have no ABI impact. Existing code remains unaffected.


Full Proposal Draft

For more detailed design, technical considerations, alternatives, and future directions, please see the full draft proposal here:

SE-NNNN: Pack Destructuring & Pack Splitting


Questions for Discussion

  • Does the motivation resonate with your experiences using variadic generics?
  • Is the proposed syntax for destructuring (let (head, repeat each tail) = ...) and splitting (process(repeat each pack)) clear and intuitive?
  • Can you think of other use cases that would benefit significantly from these features?
  • Are there any potential ambiguities or edge cases we should consider more deeply?

I look forward to hearing your thoughts and feedback!

30 Likes

Note that the proposal for pack iteration is SE-0408 not SE-0404.

Other than that I'm really excited to finally see these features!

1 Like

Thank you for review @rdemarest, updated it!

repeat each otherValues already has a meaning in that position (though I don’t know if it works today), which is “match each remaining element of the tuple being switched on with the values in otherValues”:

switch (repeat each firstValues) {
case (repeat each secondValues): 
  print("success!")
default:
  print("failure")
}

Normally repeat takes the expression or type or pattern you want to repeat, and how many times it repeats is based on the name referenced by each. But in this case, we want to define a new name, not reference an existing one, and pick up the number of elements from context. I don’t know if I have an intuitive spelling for that:

  • repeat let otherValues - normally it’s not allowed to have repeat without an each, but maybe it’s okay if it can be inferred from context?
  • each otherValues - the original pack type is defined with each, we’re making a new pack here. But what if I wanted to do a more complex match on each element, like each .blue(otherValues)? Feels like this is what repeat was for.
  • repeat let each otherValues - if the each is inside the let it can’t be mistaken for referencing an existing pack, but, keyword soup.
  • repeat each let otherValues - more soup, inconsistent with existing patterns where you can put let on the outside and it covers all names in that subpattern.

:person_shrugging:

6 Likes

@Danny Thanks for this feedback! You’re absolutely right that today’s Swift 6.1 already lets you hand‑write a two‑pack zip and even a four‑pack chain. What Multi‑Pack Operations in the Future Directions is meant to solve is:

  1. Arbitrary‑arity in one generic
    You shouldn’t need separate overloads for zip2, zip3, zip4, … forever. The goal is one generic zip that scales to any number of packs automatically.

  2. Label‑free pack parameters
    Right now, any variadic parameter after the first must carry an external label (e.g. with:) so the compiler knows where one pack ends and the next begins. I want to lift that requirement so you can write and call:

    func zip<each S1, each S2>(
      _ s1: repeat each S1,
      _ s2: repeat each S2
    ) -> (repeat each (S1, S2)) { … }
    
    let result = zip(1, 2, 3, "a", "b", "c")
    // result == ((1, "a"), (2, "b"), (3, "c"))
    

Below is a self‑contained Playground you can paste into Xcode. It shows what works today vs. what a future, fully arbitrary‑arity & label‑free API might look like (commented out).


What Works Today (Swift 6.1)

1) Two‑pack zip — second variadic requires a label

func zip<each S1, each S2>(
  _ s1: repeat each S1,
  with s2: repeat each S2
) -> (repeat(each S1, each S2)) {
  return (repeat(each s1, each s2))
}

let twoPack = zip(1, 2, 3, with: "a", "b", "c")
print(twoPack) // ((1, "a"), (2, "b"), (3, "c"))

2) Four‑pack chain — all but first pack‑param can be unlabeled

@inlinable
func chain<each A, each B, each C, each D>(
  _ a: repeat each A,
  b: repeat each B,
  c: repeat each C,
  d: repeat each D
) -> (
  repeat each A,
  repeat each B,
  repeat each C,
  repeat each D
) {
  (
    repeat each a,
    repeat each b,
    repeat each c,
    repeat each d
  )
}

let fourPack = chain(
  1, 2,
  b: "x", "y",
  c: true, false,
  d: 3.14, 2.71
)
print(fourPack) // (1, 2, "x", "y", true, false, 3.14, 2.71)

3) What fails if you drop the label today

/*
func zipFail<each S1, each S2>(
  _ s1: repeat each S1,
  _ s2: repeat each S2 // ERROR: “A parameter following a variadic parameter requires a label”
) -> (repeat(each S1, each S2)) {
  return (repeat(each s1, each s2))
}
*/

Future Directions: Multi‑Pack Operations

1) Label‑free two‑pack zip

(hypothetical — won’t compile today)

/*
func zip<each S1, each S2>(
  _ s1: repeat each S1,
  _ s2: repeat each S2
) -> (repeat(each S1, each S2)) {
  (repeat(each s1, each s2))
}

let futureTwo = zip(1, 2, 3, "a", "b", "c")
print(futureTwo) // ((1, "a"), (2, "b"), (3, "c"))
*/

2) Arbitrary‑arity pack zip

(also hypothetical for illustration)

/*
func zip<each S1, each S2, each S3>(
  _ s1: repeat each S1,
  _ s2: repeat each S2,
  _ s3: repeat each S3
) -> (repeat(each S1, each S2, each S3)) {
  (repeat(each s1, each s2, each s3))
}

let futureTri = zip(
  1,   2,   3,
  "a", "b", "c",
  true, false, true
)
print(futureTri) // ((1, "a", true), (2, "b", false), (3, "c", true))
*/

Ultimate Goal

One generic

func zip<each S: Sequence>(
  _ sequences: repeat each S
) -> ZipSequence<repeat each S>

that works label‑free for any number of sequences.


I’ve updated the Future Directions → Multi‑Pack Operations section of the proposal to spell out these exact limitations

2 Likes

Hi @jrose,

Thanks again for catching that collision, I spent some time last night thinking through how I could avoid introducing any new keywords and still fit cleanly into Swift’s existing pattern grammar. Let me know what you think, especially if you spot any edge cases!


Option A: Outer case let

switch result {
case let .success(firstValue, repeat each otherValues):
    // firstValue  : Int
    // otherValues : (String, Bool)
default:
    break
}
  • Mechanics
    • case let applies let to every subpattern in the case.
    • repeat each otherValues is our custom “bind the rest of the tuple as a new pack” pattern.
  • Pros
    • Extremely concise—follows the familiar case let .foo(...) idiom.
    • No extra punctuation or nesting needed.

Option B: Inner let + Parentheses

switch result {
case .success(
       let firstValue,
       let (repeat each otherValues)
     ):
    // firstValue  : Int
    // otherValues : (String, Bool)
default:
    break
}
  • Mechanics
    • We wrap repeat each otherValues in parentheses, turning it into a standalone tuple pattern.
    • let ( … ) is the existing Swift way to bind a tuple pattern.
  • Pros
    • Each binding site explicitly shows its own let.
    • Parentheses isolate the pack‑binding syntax from the outer pattern.

Why Both?

  1. Leverage existing grammar
    • Option A reuses case let; Option B reuses let (pattern).
  2. No ambiguity
    • In both forms, the parser knows this is a declaration of otherValues—never an expansion of a prior pack.

Quick Playground Demo

enum Result3 { case success(Int, String, Bool), failure }
let result: Result3 = .success(99, "hi", false)

// Option A
switch result {
case let .success(first, repeat each rest):
    print("A:", first, rest)   // A: 99 ("hi", false)
default: break
}

// Option B
switch result {
case .success(
       let first,
       let (repeat each rest)
     ):
    print("B:", first, rest)   // B: 99 ("hi", false)
default: break
}
2 Likes

So I really think you need to be able to push the let all the way in because I want to write this:

let pairs: (repeat (String, each T)) = …
switch pairs {
case ((let firstKey, let firstValue),
      repeat (CONST_KEY, let each remainingValue)):
  …

I mean, okay, I can’t think of a use for this offhand, but pattern syntax is supposed to be composable and we should only break that for a very good reason. Or to put it another way, writing let on the whole pattern is a shorthand; the most basic syntax is to only write it immediately before the identifiers you actually want to bind.

10 Likes

What I really want from pack destructuring/splitting is the ability to do so in/for a where clause, eg with your proposal I think I could write something like this:

struct Services<each Service> {
    var services: (repeat each Service)

    func resolve<T>(_ type: T.Type) -> T {
       _resolve(type: type, repeat each services)
    }
}

func _resolve<T, Head, each Service>(
    _ type: T.Type,
    _ head: Head,
    _ services: repeat each Service
) -> T where Head == T {
    head
}

@_disfavoredOverload
func _resolve<T, Head, each Service>(
    _ type: T.Type, 
    _ head: Head,
    _ services: repeat each Service
) -> T {
    _resolve(type: type, services)
}

func _resolve<T>(_  type: T.Type) -> T {
    fatalError()
}

But that fatalError in the base case is very unfortunate; ideally I'd like to be able to have a where clause on my top level function that statically disallows the call if the pack doesn't contain a T.

(I understand that this isn't really anything to do with what you're proposing, and that it's a whole can of worms that raises questions of overlapping conformances and conformances of non-nominal types that Swift struggles with, SE-0283 is still in limbo. Still, variadic generics have limited utility for me until this question is answered in some way, and I had high hopes that pack destructuring, when it came, might address it)

Overloads are resolved statically in Swift, and there's no _resolve() overload that matches the argument list here, so this kind of thing could not possibly work:

I'm concerned about relying on recursion. The kind of recursion seen here is familiar to people who have done functional programming or logic programming, but in my (very limited) experience, recursion tends to be unintuitive to most people, especially at first glance. I think of recursion as something primarily used to work around limitations, such as the limitations of C++ template metaprogramming. I think iteration-like or map-like techniques would be better if possible.

That makes sense, I was worried about it when typing my example. Rereading the original post it seems like guard let is defined to be able to determine the empty pack case, so I guess my example should be:

func _resolve<T, Head, each Service>(
    _ type: T.Type,
    _ services: repeat each Service
) -> T {
    if let (head, repeat each rest) = services {
        if let head = head as? T { // will this compile?
            return head
        } else {
            return _resolve(type, repeat each rest)
        }
    } else {
        fatalError()
    }
}

The desire to be able to statically avoid the fatalError() still stands, though.