https://www.swift.org/blog/pack-iteration/
* Get Started
* Blog
* Documentation
* Packages
* Community V
+ Overview
+ Swift Evolution
+ Diversity
+ Mentorship
+ Contributing
+ Workgroups
+ Contributor Experience
+ Server
+ Website
+ Language Steering Group
+ C++ Interoperability
+ Documentation
+ Governance
+ Code of Conduct
+ License
+ Security
* Download
* Get Started
* Blog
* Documentation
* Packages
* Community V
+ Overview
+ Swift Evolution
+ Diversity
+ Mentorship
+ Contributing
+ Workgroups
+ Contributor Experience
+ Server
+ Website
+ Language Steering Group
+ C++ Interoperability
+ Documentation
+ Governance
+ Code of Conduct
+ License
+ Security
* Download 5.10
Iterate Over Parameter Packs in Swift 6.0
March 7, 2024
Sima Nerush Sima Nerush
Sima Nerush is an incoming engineer on the Apple SwiftUI team. She is
an experienced iOS Engineer, Swift Compiler contributor, and a
computer science student.
Parameter packs, introduced in Swift 5.9, make it possible to write
generics that abstract over the number of arguments. This eliminates
the need to have overloaded copies of the same generic function for
one argument, two arguments, three arguments, and so on. With Swift
6.0, pack iteration makes it easier than ever to work with parameter
packs. This post will show you how to make the best use of pack
iteration.
Parameter Packs Recap
First, let's review parameter packs. Consider the following code:
let areEqual = (1, true, "hello") == (1, false, "hello")
print(areEqual)
// false
The above code simply compares two tuples. However, this code
wouldn't work if the tuples contained 7 elements!
The Swift standard library provided comparison operators for tuples
up to only 6 elements for a long time:
func == (lhs: (), rhs: ()) -> Bool
func == (lhs: (A, B), rhs: (A, B)) -> Bool where A: Equatable, B: Equatable
func == (lhs: (A, B, C), rhs: (A, B, C)) -> Bool where A: Equatable, B: Equatable, C: Equatable
// and so on, up to 6-element tuples
In each of the generic functions above, every element of the input
tuple has to have its type declared in the generic parameter list of
the function. Thus, we need to add a new element to the generic
parameter list any time we want to support a larger tuple size.
Because of this, the artificial limit of 6-element tuples was
imposed.
Parameter packs added the ability to abstract a function over a
variable number of type parameters. This means that we can lift the
6-element limit using an == operator written like this:
func == (lhs: (repeat each Element), rhs: (repeat each Element)) -> Bool
Let's break down the types we see in the above signature:
* Note each Element in the list of generic parameters. The each
keyword indicates that Element is a type parameter pack, meaning
that it can accept any number of generic arguments. Just like
with non-pack (scalar) generic parameters, we can declare a
conformance requirement on the type parameter pack. In this case,
we require each Element type to conform to the Equatable
protocol.
* This function takes in two tuples, lhs and rhs, as arguments. In
both cases, the tuple's element type is repeat each Element. This
is called the pack expansion type, which consists of a repeat
keyword followed by a repetition pattern, which has to contain a
pack reference. In our case, the repetition pattern is each
Element.
* At the call site, the user provides value parameter packs for
each tuple that will be substituted into their corresponding type
parameter packs. At runtime, the repetition pattern will be
repeated for each element in the substituted pack.
With the tuple equality operator implemented using parameter packs,
let's look at the call site again to understand these concepts
better.
let areEqual = (1, true, "hello") == (1, false, "hello")
print(areEqual)
// false
The call to == substitutes the type pack {Int, Bool, String} for the
Element type pack. Note that both lhs and rhs have the same type.
Finally, the function == is called with value packs {1, true,
"hello"} for the value pack of the lhs tuple and {1, false, "hello"}
for the value pack of the rhs tuple.
Why Pack Iteration?
The example with the new signature of the tuple comparison operator
looks great, but how do we actually use the values of the lhs and rhs
tuples inside the body of the function? Feel free to take a moment to
think about this.
It turns out that there is just no concise way of implementing the
function prior to Swift 6.0. One solution involves creating a local
function that compares a pair of elements from the two tuples, and
then using pack expansion to call that function for every pair of
elements, like this:
struct NotEqual: Error {}
func == (lhs: (repeat each Element), rhs: (repeat each Element)) -> Bool {
// Local throwing function for operating over each element of a pack expansion.
func isEqual(_ left: T, _ right: T) throws {
if left == right {
return
}
throw NotEqual()
}
// Do-catch statement for returning false as soon as two tuple elements are not equal.
do {
repeat try isEqual(each lhs, each rhs)
} catch {
return false
}
return true
}
The above code doesn't look great, right? To simply check a condition
for each pair of elements, we need to declare a local function
isEqual, that just compares the given elements. However, this is not
enough to make the function return early since the local isEqual
function will still be called on every pair of elements in the
parameter packs lhs and rhs when expanding them. Because of this,
isEqual has to be marked throws and throw an error once a pair of
mismatched elements is found. Then, we catch the error in a catch
block to return false.
Introducing Pack Iteration
Swift 6.0 greatly simplifies this task with the introduction of pack
iteration using the familiar for-in loop syntax.
More specifically, with pack iteration, the body of the == tuple
comparison operator simplifies down to a simple for-in repeat loop:
func == (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
}
In the above code, we are able to utilize the for-in loop capability
to iterate over the tuples pairwise.
Note that when iterating over packs, we use the new for-in repeat
syntax, followed by a value parameter pack that we are iterating
over. At every iteration, the loop binds each element of the value
parameter pack to a local variable. This means that in this case, the
i^th element of lhs will be bound to a local variable left on the i^
th iteration. In the body of the loop, you can use the local variable
as you normally would. In our case, we compare each pair of elements
and return false once we find a pair where left != right, using a
familiar guard statement. And, of course, we no longer need to throw
any errors as we had to before!
Using Pack Iteration
Let's now explore more ways you can utilize pack iteration in your
Swift code with some examples.
First, consider a situation where you need to write a function to
check that all arrays in a given value parameter pack are empty:
func allEmpty(_ array: repeat [each T]) -> Bool {
for a in repeat each array {
guard a.isEmpty else { return false }
}
return true
}
The above function is generic over a type parameter pack each T and
takes in a value parameter pack array, the type of which is declared
using repeat [each T] pack expansion, where [each T] is the
repetition pattern. At the call site, it is repeated for each element
in the substituted pack, resulting in values expanding into a list of
array literals.
On each iteration of the for-in repeat loop, an element of the value
parameter pack array is bound to a local variable a. Note that with
pack iteration, elements of the value pack are evaluated on demand,
meaning that we are able to return out of the function early without
examining all arrays of the value pack. In this case, we utilize the
guard statement.
Here is how you might use the allEmpty function:
print(allEmpty(["One", "Two"], [1], [true, false], []))
// False
Now, let's see an example of advanced usage of parameter packs that
is greatly simplified by pack iteration. First, let's declare the
following protocol:
protocol ValueProducer {
associatedtype Value: Codable
func evaluate() -> Value
}
The above protocol ValueProducer requires the evaluate() method
that's return type is the associated type Value that conforms to
Codable protocol.
Suppose you get a parameter pack of values of type Result
, and you need to iterate only over the success
elements and call the evaluate() method on its value. Also, suppose
you need to save the result of each call into an array. Pack
iteration makes this task super easy!
func evaluateAll(result: repeat Result) -> [any Codable] {
var evaluated: [any Codable] = []
for case .success(let valueProducer) in repeat each result {
evaluated.append(valueProducer.evaluate())
}
return evaluated
}
Let's first note the signature of the evaluateAll function. In the
generic parameter list, it declares two type parameter packs: each V:
ValueProducer, and each E: Error. Every element of the pack each V
has to conform to the protocol ValueProducer declared above, and
every element of the pack each E has to conform to the Error
protocol. The function takes in a single argument result with a pack
expansion type repeat Result. This means that the
pattern Result will be repeated for every element of
packs each V and each E at runtime.
To implement the body of the function, we first initialize the
evaluated array. Next, note how we can use the for case pattern to
execute the loop's body only for the success case of the Result enum.
We can grab the valueProducer variable, which will contain a value of
the ValueProducer type. We can now append the result of the call to
the evaluate() method to our evaluated array, which we finally
return.
Here's how you might use this function:
struct IntProducer: ValueProducer {
let contained: Int
init(_ contained: Int) {
self.contained = contained
}
func evaluate() -> Int {
return self.contained
}
}
struct BoolProducer: ValueProducer {
let contained: Bool
init(_ contained: Bool) {
self.contained = contained
}
func evaluate() -> Bool {
return self.contained
}
}
struct SomeError: Error {}
print(evaluateAll(result:
Result.success(SomeValueProducer(5)),
Result.failure(SomeError()),
Result.success(BoolProducer(true))))
// [5, true]
Summary
We are excited to bring pack iteration to Swift 6.0! As seen in this
article, pack iteration makes interacting with value parameter packs
significantly more straightforward, making such an advanced feature
more accessible and intuitive to incorporate into your Swift code.
Swift 5.10 Released
Except where otherwise noted, all content on this blog is licensed
under a Creative Commons Attribution 4.0 International license.
Copyright (c) 2024 Apple Inc. All rights reserved.
Swift and the Swift logo are trademarks of Apple Inc.
Privacy Policy Cookies
Color scheme preference ( ) Light ( ) Dark ( ) Auto