MutableSpanとMutableRawSpanの一括更新操作
Bulk update operations for MutableSpan and MutableRawSpan
このダイジェストはClaude Opus 4.7 / 4.8によって生成されたものです(License)。原文はこちら↗。
01 何が問題だったのか
SE-0467 で、UnsafeMutableBufferPointer / UnsafeMutableRawBufferPointer を安全に置き換える型として MutableSpan と MutableRawSpan が導入されました。しかし、要素をまとめて書き換える一括操作(bulk operation)は見送られていました。SE-0467 では一括更新用のメソッドも提案されていましたが、SE-0485 で、より良い命名体系を後から提案するために取り下げられていたのです。
そのため、安全なコードだけで Span の内容を MutableSpan にコピーしようとすると、要素数を自分で確認したうえで 1 要素ずつループで代入する必要がありました。
precondition(source.count == destination.count)
for i in destination.indices {
destination[i] = source[i]
}
これでは記述が冗長なだけでなく、まとめてメモリをコピーする効率的な操作も使えません。安全な型が unsafe な型を本当に置き換えるには、一括更新の手段が必要でした。
02 どのように解決されるのか
MutableSpan と MutableRawSpan に、既存の要素を上書きする update 系のメソッド群を追加します。上書きされた古い値はデイニシャライズされます。先ほどのコピーは 1 回の呼び出しで書け、一括のメモリ操作が使われます(Element が BitwiseCopyable の場合は memcpy 相当になります)。
destination.updateAll(copying: source)
命名の体系
メソッド名は「対象(destination)をどう書き換えるか」を表すベース名と、「ソース(source)をどう扱うか」を表す引数ラベルの組み合わせで構成されます。
ベース名は次の 3 種類です。
updateAll(...): span の全要素を上書きします。ソースの要素数は span と完全に一致している必要があります。updateSubrange(_:...): 指定したインデックス範囲の要素を上書きします。ソースの要素数は範囲の要素数と完全に一致している必要があります。updateElements(from:...): 指定したインデックスから順に上書きし、最後に更新した要素の次のインデックスを報告します。
ソースのラベルは次の 3 種類です。
repeating:: すべての要素を同じ値にします。copying:: ソースから要素をコピーします。moving::OutputSpanから要素をムーブします。ムーブ後のソースのメモリは未初期化になります。
この命名は、将来の range-replaceable なコンテナプロトコルで提案される removeAll / removeSubrange / replaceSubrange / insert / append などと揃えることを意図しています。
同じ値で埋める(repeating:)
var span = array.mutableSpan
span.updateAll(repeating: 0)
span.updateSubrange(2..<6, repeating: 1)
span.updateSubrange(..., repeating: 2)
updateSubrange は Range のほか、some RangeExpression(2... など)や UnboundedRange(...)も受け取れます。なお、新しい updateAll(repeating:) は既存の update(repeating:) と同じ意味ですが、既存のメソッドは非推奨にはなりません。
Span からコピーする(copying:)
var destination = array.mutableSpan
destination.updateAll(copying: source.span)
destination.updateSubrange(2..<6, copying: source.span.extracting(first: 4))
updateAll と updateSubrange では、ソースの要素数が span 全体または範囲の要素数と一致しない場合、実行時にトラップします。
要素数がわからないソースからコピーする(updateElements)
ソースから得られる要素数が事前にわからない場合は updateElements(from:copying:) を使います。ソースには SE-0516 の Iterable に適合する任意の型(Span、InlineArray、UniqueArray など)を渡せます。
var destination = array.mutableSpan
var index = 0
destination.updateElements(from: &index, copying: header)
destination.updateElements(from: &index, copying: payload)
destination.updateElements(from: &index, copying: checksum)
precondition(index == destination.count)
このオーバーロードは、イテレーションがエラーを throw しうる Iterable にも対応しています。index は inout で受け取り、正常終了時もエラーの throw 時も、最後に更新した要素の次のインデックスに更新されます。そのため、エラーが発生しても、どこまで書き換えられたかを呼び出し元が把握できます。
Iterable の Failure が Never の場合は、次のインデックスを戻り値として返す便利なオーバーロードも使えます。
var end = destination.updateElements(from: 0, copying: header)
end = destination.updateElements(from: end, copying: payload)
Iterable を受け取るオーバーロードはソースを最後まで全部コピーし、span の残りの領域に収まらない場合はトラップします。ソースが残りの領域より短いのはかまいません。
なお、ここでの from: は「どのインデックスから書き始めるか」を表します。UnsafeMutableBufferPointer の類似メソッドでは from: がコピー元を表すので、混同しないよう注意が必要です。
イテレータから途中までコピーする
3 つ目のオーバーロードは、BorrowingIteratorProtocol に適合するイテレータを inout で受け取ります。こちらは、イテレータが要素を出し尽くすか、span の末尾に達するか、どちらか早いほうで停止します。正常に返った時点で、イテレータは最後に渡した要素の次の位置を指しています。これを使うと、1 つのソースを複数の span に分けて書き込めます。
var iterator = source.makeBorrowingIterator()
var index = 0
first.updateElements(from: &index, copying: &iterator)
if index == first.count {
var index2 = 0
second.updateElements(from: &index2, copying: &iterator)
}
要素をムーブする(moving:)
updateAll(moving:) と updateSubrange(_:moving:) は、OutputSpan を inout で受け取り、その要素を span へムーブします。呼び出し後、ソースの OutputSpan は空になり、メモリは未初期化の状態に戻ります。
var destination = storage.mutableSpan
source.edit { output in
assert(output.count == 2)
destination.updateSubrange(2..<4, moving: &output)
}
assert(source.isEmpty)
ムーブには要素のコピーが不要なので、moving: 版は Element: ~Copyable の場合でも使えます。
MutableRawSpan の場合
MutableRawSpan にも同じ名前の操作が用意されます。ソースの型が Span の代わりに RawSpan、OutputSpan の代わりに OutputRawSpan になり、repeating: には UInt8 を、updateElements には要素型が UInt8 の Iterable / BorrowingIteratorProtocol を渡します。インデックスはバイトオフセット(Int)です。
var bytes = buffer.mutableBytes
bytes.updateAll(repeating: 0)
bytes.updateSubrange(0..<4, copying: magicNumber.bytes)
これらの API は新しい標準ライブラリを必要とします。ABI 安定なプラットフォームでは、updateElements は Iterable が、それ以外は Span が利用可能な OS バージョン以降で使えます。
03 今後の見通し
以下は Proposal で挙げられている将来の構想で、実現が約束されているものではありません。
OutputSpan への一括追加
OutputSpan / OutputRawSpan に対する一括の append 操作も同様の考え方で設計できますが、別の Proposal に先送りされています。本 Proposal の結果が、その設計に反映される見込みです。
同じ MutableSpan 内でのコピー
排他性の法則(Law of Exclusivity)により、コピー元とコピー先を同じ span にすることはできません。そのための専用メソッドや、既存の span を重ならない 2 つのアクセスに安全に分割する手段が将来提供される可能性があります。
non-copyable な要素を持つコンテナからの段階的な更新
updateElements(from:copying:) はコピー可能な要素に限られます。たとえば UniqueDeque<ExecutorJob> から要素をムーブして MutableSpan を更新する updateElements(from:moving:) は今回含まれていません。swift-collections の ContainersPreview モジュールにある DrainableContainer プロトコルが、その機能のプロトタイプになっています。
一般化されたコンテナプロトコル
non-copyable(および non-escapable)な要素に対応した、Collection 系プロトコルの一般化が必要とされています。そうしたプロトコル群ができれば、今回のメソッドは MutableCollection の後継プロトコルの要件になる想定です。
Collection / Sequence に適合する型からの一括コピー
現在でも IteratorProtocol のインスタンスから BorrowingIteratorAdapter を作れば、対応する updateElements のオーバーロードに渡せます。SE-0516 では既存の Sequence 型を Iterable に適合させることが先送りされましたが、適合が実現すれば、それらの型を直接 updateElements(from:copying:) に渡せるようになります。
また、より新しいコンテナプロトコルが追加されれば、updateSubrange(_:copying:) / updateSubrange(_:moving:) にも UniqueArray などを受け取れるジェネリックなオーバーロードが追加される可能性があります。