この記事の要点
- Swift 5.9 で導入された parameter packs(パラメータパック)は、引数の「個数」を抽象化したジェネリクスを書けるようにする仕組みです。これにより、1 引数版・2 引数版・3 引数版…と同じジェネリック関数を個数ごとに何度も書く必要がなくなります。
- ただし Swift 5.9 の時点では、関数本体で value parameter pack(値のパック)の各要素を扱うのが難しく、途中で抜けたい場合などはローカル関数とエラー
throwを組み合わせる回りくどい書き方が必要でした。 - Swift 6.0 で pack iteration が導入され、おなじみの
for-in構文(正確にはfor-in repeat)でパックの要素を 1 つずつ反復処理できるようになりました。guardで途中returnしたり、for caseでパターンマッチしたりも自然に書けます。
背景: parameter packs のおさらい
parameter packs が導入される前、標準ライブラリはタプル同士の比較演算子 == を、要素数ごとに別々のジェネリック関数として用意していました。
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
// 以下、6 要素のタプルまで続く
要素数を 1 つ増やすたびにジェネリックパラメータと新しいオーバーロードが必要になるため、長らく「6 要素まで」という人為的な上限が課されていました。
parameter packs は、関数を可変個の型パラメータについて抽象化できるようにします。これを使うと、== を 1 つのシグネチャで書けるようになり、6 要素の制限を取り払えます。
func == <each Element: Equatable>(lhs: (repeat each Element), rhs: (repeat each Element)) -> Bool
シグネチャに登場する要素を読み解くと次のとおりです。
- ジェネリックパラメータの
each Elementは type parameter pack(型パラメータパック) を表します。eachキーワードは、Elementが任意個の型引数を受け取れることを示します。通常の(スカラーの)ジェネリックパラメータと同様に適合要件を課すこともでき、ここでは各ElementがEquatableに適合することを要求しています。 - 引数
lhs/rhsの型repeat each Elementは pack expansion type(パック展開型) と呼ばれます。repeatキーワードに続けて、パック参照を含む繰り返しパターン(ここではeach Element)を書きます。 - 呼び出し側は、各タプルに対応する value parameter pack(値パラメータパック) を渡します。実行時には、置き換えられたパックの各要素について繰り返しパターンが展開されます。
このシグネチャに対して (1, true, "hello") == (1, false, "hello") のように呼び出すと、型パック {Int, Bool, String} が Element に、値パック {1, true, "hello"} と {1, false, "hello"} がそれぞれ lhs / rhs に対応づけられます。
pack iteration がなぜ必要だったのか
シグネチャは書けても、Swift 6.0 より前には関数本体で lhs / rhs の各要素を簡潔に扱う方法がありませんでした。要素を 1 ペアずつ比較し、不一致を見つけ次第 false を返したいだけなのに、次のような回りくどい書き方が必要でした。
struct NotEqual: Error {}
func == <each Element: Equatable>(lhs: (repeat each Element), rhs: (repeat each Element)) -> Bool {
// パック展開の各要素に対して処理するためのローカル関数。
func isEqual<T: Equatable>(_ left: T, _ right: T) throws {
if left == right {
return
}
throw NotEqual()
}
// 不一致を見つけた時点で false を返すための do-catch。
do {
repeat try isEqual(each lhs, each rhs)
} catch {
return false
}
return true
}
パック展開ではすべての要素についてローカル関数 isEqual が呼ばれてしまうため、途中で打ち切るには isEqual を throws にしてエラーを throw し、それを catch で受けて false を返す、という遠回りが避けられませんでした。
pack iteration を使う
Swift 6.0 では、おなじみの for-in 構文を使った pack iteration が導入され、この処理が大幅に簡潔になります。パックを反復するときは、for-in repeat の後ろに反復対象の value parameter pack を書きます。
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
}
各反復で、value parameter pack の i 番目の要素がローカル変数(ここでは left / right)に束縛され、ループ本体では通常の変数と同じように使えます。guard で途中 return できるため、もはやエラーを throw する必要はありません。
要素を 必要なときに 1 つずつ評価する 点も重要です。次の「すべての配列が空かどうか」を確かめる関数では、空でない配列を見つけた時点で、残りを調べずに早期 return できます。
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
さらに、for case を使ったパターンマッチも組み合わせられます。次の evaluateAll は、Result のパックを受け取り、.success の要素についてだけ evaluate() を呼んで結果を集めます。
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
}
for case .success(let valueProducer) により、.success の場合だけループ本体が実行され、取り出した valueProducer に対して evaluate() を呼べます。.failure の要素はスキップされます。
print(evaluateAll(result:
Result<IntProducer, SomeError>.success(IntProducer(5)),
Result<SomeProducer, SomeError>.failure(SomeError()),
Result<BoolProducer, SomeError>.success(BoolProducer(true))))
// [5, true]
ここで IntProducer / BoolProducer は、それぞれ Int / Bool を返す ValueProducer の実装だとします。SomeProducer は失敗ケースを表すためのプレースホルダーの型で、.failure の要素はスキップされ値を取り出さないため、その具体的な型は結果に影響しません。
まとめ
pack iteration は、for-in repeat という見慣れた構文で value parameter pack を反復処理できるようにし、guard による早期 return や for case によるパターンマッチも自然に書けるようにします。これにより、parameter packs という高度な機能が、より直感的に扱えるようになりました。
関連リンク
- SE-0408: Pack Iteration — pack iteration を Swift 6.0 に導入した Proposal
- SE-0393: Value and Type Parameter Packs — parameter packs を Swift 5.9 に導入した Proposal