- Published at
Value and Type Parameter Packs in Swift

Explore the implementation of value and type parameter packs in Swift 5.9. Understand how they enable variadic generics, improve library maintenance, and enhance developer experience.
- Authors
-
-
- Name
- Andy Kolean
-
Table of Contents
1. Introduction
Swift 5.9 introduced a groundbreaking feature that significantly enhances the language’s capability to handle variadic generics: value and type parameter packs. Authored by Holly Borla, John McCall, and Slava Pestov, the proposal SE-0393 brings a new level of abstraction to Swift, allowing functions to operate over an arbitrary number of types and values.
In traditional Swift programming, creating functions that accept a variable number of arguments often required defining multiple overloads. This approach, while effective, imposed limitations and increased maintenance complexity. The introduction of value and type parameter packs in Swift 5.9 addresses these challenges by providing a more elegant and scalable solution.
This blog post aims to provide an in-depth exploration of value and type parameter packs. We will begin by understanding the motivation behind their introduction and the limitations they overcome. Then, we will dive into the implementation details, exploring how to use these parameter packs effectively in your Swift code. Finally, we will look at practical examples and advanced use cases to help you fully grasp the power of this feature.
Whether you are a seasoned Swift developer or new to the language, this post will equip you with the knowledge to leverage value and type parameter packs in your projects, making your code more flexible and maintainable.
2. Motivation
In the realm of Swift programming, there has always been a need to handle functions that accept a variable number of arguments. Traditionally, Swift developers have addressed this need with ad-hoc variadic APIs that rely on overloads for each possible number of arguments. This approach, while functional, has significant drawbacks:
- Complexity: Maintaining multiple overloads for different numbers of parameters increases the complexity of the codebase.
- Boilerplate Code: Each additional overload adds boilerplate code, which is repetitive and error-prone.
- Scalability: There is an artificial limit on the number of arguments that can be handled, typically capped at a small number, such as six.
Consider the example of tuple comparison operators in the Swift standard library. To compare tuples of different lengths, the library provides separate overloads for each tuple size up to six elements:
func == (lhs: (), rhs: ()) -> Bool
func == <A, B>(lhs: (A, B), rhs: (A, B)) -> Bool where A: Equatable, B: Equatable
func == <A, B, C>(lhs: (A, B, C), rhs: (A, B, C)) -> Bool where A: Equatable, B: Equatable, C: Equatable
// and so on, up to 6-element tuples
This limitation not only makes the library harder to maintain but also forces developers to write additional overloads whenever they need to support tuples with more elements.
To address these challenges, Swift 5.9 introduces value and type parameter packs. These packs allow developers to abstract over the number of parameters, significantly simplifying the implementation of variadic APIs. By leveraging parameter packs, a single function can now handle an arbitrary number of arguments, reducing boilerplate code and making the codebase more maintainable.
3. Understanding Value and Type Parameter Packs
Value and type parameter packs are the core concepts introduced in Swift 5.9 that facilitate variadic generics. These parameter packs allow functions to accept and operate on an arbitrary number of arguments or type parameters. Let’s break down these concepts:
Type Parameter Packs
A type parameter pack is a way to abstract over a variable number of type parameters in a generic function. Instead of defining multiple overloads for different numbers of type parameters, a single type parameter pack can handle them all. This is achieved using the each
keyword.
For example, consider the following function that can compare tuples of any size, as long as their elements conform to the Equatable
protocol:
func == <each Element: Equatable>(lhs: (repeat each Element), rhs: (repeat each Element)) -> Bool
In this function:
each Element
is a type parameter pack that can accept any number of type parameters.repeat each Element
is a pack expansion that repeats the pattern for each element in the pack.
At the call site, the type parameter pack Element
is substituted with a concrete list of types, and the pack expansion repeats the pattern for each type in the pack.
Value Parameter Packs
Similarly, a value parameter pack allows functions to accept a variable number of arguments. These are also declared using the each
keyword, and the pack expansion syntax is the same.
Consider a function that creates pairs from two lists of values:
struct Pair<First, Second> {
let first: First
let second: Second
}
func makePairs<each First, each Second>(
firsts first: repeat each First,
seconds second: repeat each Second
) -> (repeat Pair<each First, each Second>) {
return (repeat Pair(first: each first, second: each second))
}
In this function:
each First
andeach Second
are type parameter packs.repeat each First
andrepeat each Second
are value parameter packs that repeat the pattern for each element in the pack.
At the call site, the type parameter packs First
and Second
are substituted with concrete lists of types, and the value parameter packs first
and second
are substituted with lists of values.
Pack Expansion Types
A pack expansion type is a new kind of type that allows you to use parameter packs in various positions where a list of types or values is expected. The repeat
keyword followed by a pattern that contains a pack reference creates a pack expansion type.
For instance, in the makePairs
function, the return type (repeat Pair<each First, each Second>)
is a tuple type that contains a pack expansion type. It expands to a tuple where each element is a Pair
of the corresponding elements from the First
and Second
packs.
Key Concepts
- Scalar Types and Values: All existing types and values in Swift are considered scalar types and values under the new model.
- Type Packs and Value Packs: A type pack is a list of scalar types, and a value pack is a list of scalar values.
- Pack Expansion: Pack expansions allow patterns to be repeated for each element in a type or value pack.
4. Implementing Value and Type Parameter Packs
In this section, we will explore how to implement value and type parameter packs in Swift, along with the rules and restrictions that apply to their use.
Type Parameter Packs
Type parameter packs allow you to define a generic function that can accept an arbitrary number of type parameters. Let’s look at the syntax and some examples:
// 'T' is a type parameter pack where each element conforms to 'Equatable'.
func compareTuples<each T: Equatable>(lhs: (repeat each T), rhs: (repeat each T)) -> Bool {
for (left, right) in repeat (each lhs, each rhs) {
guard left == right else { return false }
}
return true
}
In this example:
each T
declares a type parameter pack.repeat each T
creates a pack expansion type that repeats the pattern for each element in the pack.
Value Parameter Packs
Value parameter packs allow functions to accept a variable number of arguments. Here’s how you can use them:
struct Pair<First, Second> {
let first: First
let second: Second
}
func makePairs<each First, each Second>(
firsts first: repeat each First,
seconds second: repeat each Second
) -> (repeat Pair<each First, each Second>) {
return (repeat Pair(first: each first, second: each second))
}
let pairs = makePairs(firsts: 1, "hello", seconds: true, 2.0)
// 'pairs' is '(Pair(1, true), Pair("hello", 2.0))'
In this example:
each First
andeach Second
declare type parameter packs.repeat each First
andrepeat each Second
create value parameter packs.- The return type
(repeat Pair<each First, each Second>)
is a pack expansion type that produces a tuple ofPair
instances.
Type Matching and Substitution
When using type parameter packs, Swift needs to match and substitute types correctly. Here are the rules that apply:
- Label Matching: Labels are used to delimit type packs. If a function declaration has a pack expansion type, the parameter must be the last parameter or followed by a labeled parameter.
- Type List Matching: Matching two lists of types involves comparing the common prefix and suffix. If either list contains pack expansions, the match must be solved by other means.
- Same-Shape Requirements: Type parameter packs must have the same number of elements, with pack expansion types at identical positions.
5. Examples and Use Cases
In this section, we will explore practical examples of using value and type parameter packs in Swift. These examples will demonstrate how to implement variadic functions, compare tuples, use pack iteration, and handle advanced use cases. By understanding these examples, you will see how parameter packs can make your code more flexible and maintainable.
Implementing Variadic Functions with Parameter Packs
One of the primary use cases for value and type parameter packs is to implement functions that can accept a variable number of arguments or type parameters. This is particularly useful when you want to avoid writing multiple overloads for different numbers of parameters.
We saw this earlier with the function that creates pairs from two lists of values:
struct Pair<First, Second> {
let first: First
let second: Second
}
func makePairs<each First, each Second>(
firsts first: repeat each First,
seconds second: repeat each Second
) -> (repeat Pair<each First, each Second>) {
return (repeat Pair(first: each first, second: each second))
}
let pairs = makePairs(firsts: 1, "hello", seconds: true, 2.0)
// 'pairs' is '(Pair(1, true), Pair("hello", 2.0))'
In this example:
each First
andeach Second
declare type parameter packs. This allows the function to accept any number of type parameters.repeat each First
andrepeat each Second
create value parameter packs. This syntax tells Swift thatfirst
andsecond
are lists of values, not single values.- The return type
(repeat Pair<each First, each Second>)
is a pack expansion type that produces a tuple ofPair
instances. This allows the function to return a tuple where each element is aPair
constructed from the corresponding elements offirst
andsecond
.
When to Use: Use this pattern when you need to handle functions that can take multiple arguments of potentially different types, such as in generic libraries or utility functions that operate on collections of heterogeneous data.
Comparing Tuples with Parameter Packs
Parameter packs simplify the implementation of functions that need to compare tuples of arbitrary length. Instead of defining multiple overloads, you can use a single function with parameter packs.
Here’s an example of a tuple comparison operator:
func == <each Element: Equatable>(lhs: (repeat each Element), rhs: (repeat each Element)) -> Bool {
for (left, right) in repeat (each lhs, each rhs) {
guard left == right else { return false }
}
return true
}
let areEqual = (1, true, "hello") == (1, false, "hello")
// 'areEqual' is 'false'
In this example:
each Element
is a type parameter pack that can accept any number of type parameters, provided each type conforms toEquatable
.repeat each Element
creates a pack expansion type for the tuple elements.- The
for
loop withrepeat
iterates over the elements of the tuples, comparing them pairwise. If any pair of elements are not equal, the function returnsfalse
.
When to Use: Use this pattern when you need to compare collections or tuples of varying lengths where the elements conform to a specific protocol, such as Equatable
.
Using Pack Iteration to Simplify Code
Pack iteration allows you to iterate over the elements of a parameter pack using a familiar for-in
loop syntax. This makes it easier to write functions that operate on multiple elements.
Here’s an example of a function that checks if all arrays in a value parameter pack are empty:
func allEmpty<each T>(_ array: repeat [each T]) -> Bool {
for a in repeat each array {
guard a.isEmpty else { return false }
}
return true
}
print(allEmpty(["One", "Two"], [1], [true, false], []))
// 'false'
In this example:
each T
is a type parameter pack.repeat [each T]
creates a value parameter pack for the arrays.- The
for
loop withrepeat
iterates over the arrays, checking if each one is empty. If any array is not empty, the function returnsfalse
.
When to Use: Use this pattern when you need to perform the same operation on multiple collections or sequences, such as checking conditions, transforming elements, or aggregating results.
Concatenating Strings
Here’s an example of a function that concatenates multiple strings using value parameter packs:
func concatenate<each T: StringProtocol>(_ strings: repeat each T) -> String {
var result = ""
for str in repeat each strings {
result += str
}
return result
}
print(concatenate("Hello, ", "world", "!")) // 'Hello, world!'
In this example:
each T: StringProtocol
declares a type parameter pack that conforms toStringProtocol
.repeat each T
creates a value parameter pack for the strings.- The
for
loop withrepeat
iterates over the strings, concatenating them into a single result string.
When to Use: Use this pattern when you need to concatenate or combine multiple values, especially when dealing with strings or other sequences.
Finding the Maximum in Arrays
Here’s an example of a function that finds the maximum element in multiple arrays using value parameter packs:
public static func maxInArrays<each T: Sequence>(_ arrays: repeat each T) -> (repeat (each T).Element?) where repeat (each T).Element: Comparable {
return (repeat (each arrays).max())
}
let maxValues = maxInArrays([1, 3, 5], ["a", "b", "c"], [10.5, 3.2, 8.6])
// 'maxValues' is '(Optional(5), Optional("c"), Optional(10.5))'
In this example:
each T: Sequence
declares a type parameter pack that conforms toSequence
.repeat each T
creates a value parameter pack for the arrays.- The return type
(repeat (each T).Element?)
is a pack expansion type that produces a tuple of optional elements, where each element is the maximum value from the corresponding array. - The function uses the
max()
method to find the maximum value in each array.
When to Use: Use this pattern when you need to find the maximum value in multiple collections or sequences, especially when dealing with arrays of different types.
Advanced Use Cases with Parameter Packs
Parameter packs enable advanced use cases that involve more complex patterns and transformations. Here’s an example of a function that evaluates a list of ValueProducer
instances and collects their results:
protocol ValueProducer {
associatedtype Value: Codable
func evaluate() -> Value
}
func evaluateAll<each V: ValueProducer, each E: Error>(result: repeat Result<each V, each E>) -> [any Codable] {
var evaluated: [any Codable] = []
for case .success(let valueProducer) in repeat each result {
evaluated.append(valueProducer.evaluate())
}
return evaluated
}
struct IntProducer: ValueProducer {
let contained: Int
func evaluate() -> Int {
return contained
}
}
struct BoolProducer: ValueProducer {
let contained: Bool
func evaluate() -> Bool {
return contained
}
}
struct SomeError: Error {}
print(evaluateAll(result:
Result<IntProducer, SomeError>.success(IntProducer(contained: 5)),
Result<BoolProducer, SomeError>.failure(SomeError()),
Result<BoolProducer, SomeError>.success(BoolProducer(contained: true))))
// '[5, true]'
In this example:
each V
andeach E
are type parameter packs.repeat Result<each V, each E>
creates a value parameter pack for the results.- The
for case
pattern iterates over the successful results, evaluating theValueProducer
instances and collecting their results.
When to Use: Use this pattern when you need to process heterogeneous collections of results, such as handling multiple asynchronous operations or processing different types of data producers.
By leveraging value and type parameter packs, you can create more flexible and maintainable code that handles a variable number of arguments or type parameters. These examples demonstrate the power and versatility of parameter packs in real-world scenarios.
6. Conclusion
In this post, we explored the powerful new feature introduced in Swift 5.9: value and type parameter packs. By allowing functions to accept and operate on an arbitrary number of arguments or type parameters, parameter packs significantly enhance the flexibility and maintainability of Swift code.
We began by understanding the motivation behind the introduction of parameter packs, addressing the limitations of traditional variadic APIs that rely on multiple overloads. Then, we delved into the implementation details, learning how to define and use type and value parameter packs effectively.
Through practical examples, we saw how parameter packs can be used to implement variadic functions, compare tuples, iterate over collections, and handle advanced use cases such as evaluating heterogeneous collections of results. These examples demonstrated the versatility and power of parameter packs in real-world scenarios.
By leveraging value and type parameter packs, Swift developers can write more concise, readable, and maintainable code. Whether you’re creating generic libraries, utility functions, or complex data processing pipelines, parameter packs provide a robust solution for handling variable numbers of arguments and type parameters.
As you incorporate parameter packs into your Swift projects, you’ll find that they open up new possibilities for abstraction and code reuse, making your codebase more robust and scalable. We encourage you to experiment with parameter packs and explore the many ways they can simplify and enhance your development workflow.