Pack Iteration
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
}
lhs と rhs の i 番目の要素が (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 を受け取る zip の Iterator.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)
}