Swift Digest
SE-0408 | Swift Evolution

Pack Iteration

Proposal
SE-0408
Authors
Sima Nerush, Holly Borla
Review Manager
Doug Gregor
Status
Implemented (Swift 6.0)

01 何が問題だったのか

SE-0393 で parameter pack が導入され、値パックの各要素に対する一律の操作は repeat によるパック展開式で書けるようになりました。しかし、パック展開式はあくまで なので、次のようなループ的な操作は表現できません。

  • 条件を満たした時点で break / continue して途中で打ち切る
  • ループ本体に文(ステートメント)を書く

現状、値パックの各要素に対してこれらを行うには、パック展開式の中で throw する関数を呼び、外側の do / catch で拾って脱出するという不自然なイディオムが必要でした。たとえば任意要素数のタプル同士に対する == は、次のように書かざるを得ません。

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()
  }

  do {
    repeat try isEqual(each lhs, each rhs)
  } catch {
    return false
  }

  return true
}

要素が等しくないことを throw でしか伝えられず、本来の「早期 return」ではなく NotEqual というエラー型とそれを拾う do / catch を経由する必要があります。これは短絡評価のためだけの見かけ上の throwing であり、Swiftにおけるループの書き方として自然とは言えません。パック展開式は常に「パックの長さ分だけ評価される」ので、途中で止められないこと自体がこのぎこちなさの根本原因でした。

02 どのように解決されるのか

for-in ループのソース(in の右辺)として、Sequence に適合する式だけでなく パック展開式 も書けるようにします。これにより、値パックの各要素を順にローカル変数へバインドしながら、通常のループと同じ感覚で文を書いたり break / continue で短絡したりできるようになります。

先ほどのタプル用の == は、次のように素直に書き下せます。

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
}

lhsrhsi 番目の要素が (left, right) として取り出され、一致しなければその場で return false できます。

反復変数の型

for のソースがパック展開式 repeat P の場合、i 回目の反復では、パターン P がキャプチャしている型パラメータパックの i 番目の要素が、それぞれ対応する「暗黙のスカラー型パラメータ」に置き換わります。反復変数の型は、この置き換えで得られるスカラー型です。

func iterate<each Element>(over element: repeat each Element) {
  for element in repeat each element {
    // i 回目の反復では、element の型は Element の i 番目の要素
  }
}

パックに制約がある場合、反復変数のスカラー型パラメータも同じ制約を引き継ぎます。たとえば each Element: P のもとでは次のようになります。

struct Generic<T> {}
protocol P {}

func iterate<each Element: P>() {
  for x in repeat Generic<each Element>() {
    // x の型は <Element': P> Generic<Element'>
  }
}

パターンマッチ

通常の for-in と同様に、パック版でも各要素に対してパターンマッチを使えます。

enum E<T> {
  case one(T)
  case two
}

func iterate<each Element>(over element: repeat E<each Element>) {
  for case .one(let value) in repeat each element {
    // value の型は各要素に対応するスカラー型パラメータ
  }
}

パターン式の評価タイミング

通常のパック展開式はパックの長さ分だけ即時に評価されますが、for-in のソースとしてのパターン式は 反復ごとに1回だけ 評価されます。つまり、ループ本体で break した場合、残りの要素に対するパターン式の評価は行われません。

func printAndReturn<Value>(_ value: Value) -> Value {
  print("Evaluated pack element value \(value)")
  return value
}

func iterate<each T>(_ t: repeat each T) {
  var i = 0
  for value in repeat printAndReturn(each t) {
    print("Evaluating loop iteration \(i)")
    if i == 1 {
      break
    } else {
      i += 1
    }
  }
  print("Done iterating")
}

iterate(1, "hello", true)

実行結果は次のようになり、3番目の要素 true に対応する printAndReturn は呼ばれていないことがわかります。

Evaluated pack element value 1
Evaluating loop iteration 0
Evaluated pack element value hello
Evaluating loop iteration 1
Done iterating

今後の見通し

この提案のスコープは for-in によるパックの反復に限られますが、関連する方向性として、パック展開式に対する guard let の許可が議論されています。これが入ると、たとえば任意個数の Sequence を受け取る zipIterator.next() を、次のように「各イテレータの次要素がひとつでも nil なら nil を返す」という自然な形で書けるようになります(speculativeであり、実現を約束するものではありません)。

public mutating func next() -> Element? {
  if reachedEnd {
    return nil
  }

  // パック展開式に対する guard let(将来の拡張)
  guard let element = repeat (each iterators).next() else {
    return nil
  }

  return (repeat each element)
}