BorrowingIteratorProtocol.skip(by:) の inout オーバーロード
inout overload of BorrowingIteratorProtocol.skip(by:)
このダイジェストはClaude Opus 4.7 / 4.8によって生成されたものです(License)。原文はこちら↗。
01 何が問題だったのか
SE-0516 で導入された Iterable は、通常は for-in で反復しますが、イテレータを手動で操作すれば、取り出す要素や読み飛ばす要素の単位を細かく制御できます。要素を読み飛ばすためのメソッドとして、BorrowingIteratorProtocol には skip(by:) が用意されています。
既存の skip(by:) は、実際に読み飛ばせた要素数を戻り値として返します。要求した数より少なければ、イテレータが末尾に達したことがわかります。
var iterator = myIterable.makeBorrowingIterator()
// existing version
do {
let skipCount = try iterator.skip(by: 5)
if skipCount == 5 {
print("skipped 5 elements")
} else {
print("reached end of iterator")
}
} catch {
// no access to skipCount
}
しかし、イテレータが途中でエラーを throw した場合、戻り値は返されないため、エラーが起きるまでに何要素を読み飛ばせたのか という情報が失われてしまいます。エラー後も処理を再開できるように設計されたイテレータでは、この情報がないと「残りあと何要素を読み飛ばせばよいのか」がわからず、リトライのような処理を正しく書けませんでした。
02 どのように解決されるのか
BorrowingIteratorProtocol に、オフセットを inout Int で受け取る skip(by:) のオーバーロードを追加します。呼び出し後、この引数には まだ読み飛ばせていない残りの要素数 が書き込まれます。正常に戻った場合だけでなく、エラーを throw した場合も同様に更新されます。
var iterator = myIterable.makeBorrowingIterator()
// new version
var toSkip = 5
do {
try iterator.skip(by: &toSkip)
if toSkip == 0 {
print("skipped 5 elements")
} else {
print("reached end of iterator")
}
} catch {
print("threw an error, still to skip: \(toSkip)")
}
既存の skip(by:) と新しいオーバーロードは、引数を通常の値として渡すか & を付けて inout で渡すかで呼び分けられます。
引数の値は「残り」の要素数
戻り値版が「読み飛ばせた数」を返すのに対し、inout 版の引数は「読み飛ばせなかった数」を表す点に注意が必要です。たとえば 5 を渡して4要素だけ読み飛ばせた場合、呼び出し後の値は 1 になります。正常に要求数をすべて読み飛ばせた場合は 0 になり、0 より大きい値で正常に戻った場合はイテレータが末尾に達したことを意味します。渡す値は0以上でなければなりません。
この向きにしているのは、続きの処理にそのまま使える値だからです。同じ変数を渡して skip(by:) を呼び直すだけで、残りの分の読み飛ばしを続けられます。これは UniqueArray.formIndex(_:offsetBy:limitedBy:) が「移動できなかった距離」を報告する振る舞いとも揃っています。
エラー後の再開に使う
BorrowingIteratorProtocol は、エラーを throw した後のイテレータの振る舞いについて何も要求していません。そのため、汎用コードではこの値が役に立つとは限りません。一方、エラー後も安全に処理を再開できることを独自に保証しているイテレータでは、この新しいオーバーロードがリトライなどのアルゴリズムを組み立てる基本部品になります。
たとえば、エラーが起きた直後でも再開できるように設計された MyConcreteIterator であれば、次のように一定回数までリトライしながら読み飛ばすメソッドを書けます。
extension MyConcreteIterator {
func nextElement(afterSkipping n: Int, retryCount: Int = 0) throws -> Span<Element> {
var retryCount = retryCount
var toSkip = n
while toSkip > 0 {
do {
try skip(by: &toSkip)
guard toSkip == 0 else {
return Span()
}
} catch {
if retryCount > 0 {
retryCount -= 1
continue
}
throw error
}
}
return nextSpan(maxCount: 1)
}
}
skip(by:) を呼ぶたびに、実際に読み飛ばせた分だけ toSkip が減るので、呼び出し側で残数を計算し直す必要がありません。
追加されるAPIとデフォルト実装
BorrowingIteratorProtocol には次の要求が追加されます。
public protocol BorrowingIteratorProtocol<Element, Failure>: ~Copyable, ~Escapable {
// Other existing declarations...
mutating func skip(by maxOffset: Int) throws(Failure) -> Int
mutating func skip(by offset: inout Int) throws(Failure)
}
標準ライブラリは両方のオーバーロードにデフォルト実装を提供します。デフォルトの inout 版は、nextSpan(maxCount:) を繰り返し呼び出し、返された Span の要素数だけ offset を減らしていく実装です。これは、nextSpan(maxCount:) がエラーを throw する前に、それまでに生成した要素を必ず返さなければならないという BorrowingIteratorProtocol の要件に基づいています。
また、既存の戻り値版のデフォルト実装は、利用可能な環境では新しい inout 版に転送されます。そのため、独自のイテレータを実装する場合は inout 版だけをカスタマイズすれば、両方のオーバーロードにその実装が反映されます。これまで戻り値版の skip(by:) を独自に実装していたイテレータの作者は、この転送の恩恵を受けるために、新しい inout 版の実装に切り替えることが推奨されています。