Swift Digest
Blog | Swift.org Blog

Swift 6.0 で parameter packs を反復処理する

Iterate Over Parameter Packs in Swift 6.0

このダイジェストはClaude Opus 4.7 / 4.8によって生成されたものです(License)。原文はこちら

この記事の要点

背景: 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

シグネチャに登場する要素を読み解くと次のとおりです。

このシグネチャに対して (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 が呼ばれてしまうため、途中で打ち切るには isEqualthrows にしてエラーを 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 による早期 returnfor case によるパターンマッチも自然に書けるようにします。これにより、parameter packs という高度な機能が、より直感的に扱えるようになりました。

関連リンク