[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!

23 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:

5 Likes

Without giving this as much thought as it deserves, it looks useful to me.

However, I'm unclear on what Future Directions/Multi‑Pack Operations would entail.

For one, zip already works.

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

Is that where the following would be addressed?

/// Concatenate a bunch of tuples into one.
/// This overload is specifically for 4 tuples.
@inlinable public func chain<
  each Element0,
  each Element1,
  each Element2,
  each Element3
>(
  _ element0: (repeat each Element0),
  _ element1: (repeat each Element1),
  _ element2: (repeat each Element2),
  _ element3: (repeat each Element3)
) -> (
  repeat each Element0,
  repeat each Element1,
  repeat each Element2,
  repeat each Element3
) {
  ( repeat each element0,
    repeat each element1,
    repeat each element2,
    repeat each element3
  )
}
1 Like

@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

1 Like

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.

8 Likes