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!