Type parameter packs and value parameter packs allow you to write a generic function that accepts an arbitrary number of arguments with distinct types. As a result of SE-393, SE-398, and SE-399, you can use this new feature from Swift 5.9.
One of the most noticeable places of impact is the 10-view limit in SwiftUI, which no longer exists due to variadic generics that became possible after these proposals. It’s also likely that the underlying code you’ve already used is now rewritten using parameter packs. It’s an advanced feature in Swift, but let’s see how you can benefit from using it in your projects.
Before diving into this topic, I recommend familiarizing yourself with generics by reading the following articles first:
- Generics in Swift explained with code examples
- Existential any in Swift explained with code examples
- Some keyword in Swift: Opaque types explained with code examples
What are type and value parameter packs?
Type and value parameter packs are always used together. Their length matches, and the corresponding input index equals the output index. Without context this is hard to understand, so let’s dive into an example:
func eachFirst<each T: Collection>(_ item: repeat each T) -> (repeat (each T).Element?) {
return (repeat (each item).first)
}
In this example, we’ve defined a new global method called eachFirst
, allowing us to pass in any arbitrary number of arrays and get the same number of optional elements as a return value.
We’ve first defined the type parameter pack to be each T: Collection
. In other words, we have a type parameter pack of collections. The same applies to the function argument repeat each T
, which tells us we will repeat each generic input collection.
The function returns a value parameter pack. In this case, it’s a value pack containing all the first elements of the input collections.
As an example, we’re going to use the method using a collection of integers and names:
let numbers = [0, 1, 2]
let names = ["Antoine", "Maaike", "Sep"]
let firstValues = eachFirst(numbers, names)
print(firstValues) // Optional(0), Optional("Antoine")
As I mentioned before, the input index matches the output index. We pass the numbers array as the first argument, and its first value is returned as the first item inside the produced value pack.
What parameter packs solve
Parameter packs help us write reusable code and prevent us from writing many overloads. Before parameter packs, we would’ve had to write a number of overloads as follows:
func eachFirst<T>(
_ item: T
) -> T?
func eachFirst<T1, T2>(
_ item1: T1,
_ item2: T2
) -> (T1?, T2?)
func eachFirst<T1, T2, T3>(
_ item1: T1,
_ item2: T2,
_ item3: T3
) -> (T1?, T2?, T3?)
You might recognize these overloads from Combine operators like zip
, combineLatest
, and merge
. These overloads were also the reason for the 10-view limit in SwiftUI, as the view’s body parameter uses the @ViewBuilder
underlying build block method, which was defined as follows:
static func buildBlock<C0, C1, C2, C3, C4, C5, C6, C7, C8, C9>(_ c0: C0, _ c1: C1, _ c2: C2, _ c3: C3, _ c4: C4, _ c5: C5, _ c6: C6, _ c7: C7, _ c8: C8, _ c9: C9) -> TupleView<(C0, C1, C2, C3, C4, C5, C6, C7, C8, C9)> where C0: View, C1: View, C2: View, C3: View, C4: View, C5: View, C6: View, C7: View, C8: View, C9: View {
return .init((c0, c1, c2, c3, c4, c5, c6, c7, c8, c9))
}
The above example is the final overload method of all build block overloads:
Starting from Swift 5.9, the same method is rewritten using type and value parameter packs:
static func buildBlock<each Content>(_ content: repeat each Content) -> TupleView<(repeat each Content)> where repeat each Content : View
Why can’t I just use arrays instead?
One of the first questions I had was whether I can’t just use arrays instead. However, the example I shared before would not have been possible without type erasure.
To demonstrate this, I’ve rewritten the earlier example using generics only:
func eachFirst<T: Collection>(collections: [T]) -> [T.Element?] {
collections.map(\.first)
}
Once we pass in the same numbers and names arguments, we would run into the following error:
Since we use arrays of ints and strings as input parameters, the compiler can’t produce a resulting value of the same type.
Requiring a minimum argument length
The methods you write using type and value parameter packs likely require at least one argument to be helpful. For example, our eachFirst
method is worthless when we don’t pass any value and it even results in a warning:
let firstValues = eachFirst()
// Warning: Constant 'firstValues' inferred to have type '()', which may be unexpected
Therefore, it’s recommended to rewrite your methods by using a leading argument to require at least one argument to be passed:
func eachFirst<FirstT: Collection, each T: Collection>(_ firstItem: FirstT, _ item: repeat each T) -> (FirstT.Element?, repeat (each T).Element?) {
return (firstItem.first, repeat (each item).first)
}
The final result allows us to pass in one or many arrays:
let numbers = [0, 1, 2]
let names = ["Antoine", "Maaike", "Sep"]
let booleans = [true, false, true]
let doubles = [3.3, 4.1, 5.6]
let firstValues = eachFirst(numbers, names, booleans, doubles)
print(firstValues) // (Optional(0), Optional("Antoine"), Optional(true), Optional(3.3))
Conclusion
Value and type parameter packs allow us to reduce the number of overloads and write generic functions that accept an arbitrary number of arguments with distinct types. While it’s an advanced feature of Swift, you might recognize similar overloads inside your codebase that you can start rewriting using parameter packs.
If you like to improve your Swift knowledge, even more, check out the Swift category page. Feel free to contact me or tweet to me on Twitter if you have any additional tips or feedback.
Thanks!