Swift Digest
SE-0488 | Swift Evolution

Apply the extracting() slicing pattern more widely

Proposal
SE-0488
Authors
Guillaume Lessard
Review Manager
Tony Allevato
Status
Implemented (Swift 6.2)

01 何が問題だったのか

通常の Collectionsubscript でスライスを取り出せますが、このパターンはそのままでは Span / RawSpan のような non-escapable な型や、non-copyable な要素を扱う型には適用できません。subscript の戻り値はそのまま外に出せる前提なので、ライフタイムが呼び出し元と紐付く non-escapable な値を返すのには向かないためです。

そこで SE-0437UnsafeBufferPointer などに extracting() メソッド群が導入され、SE-0467MutableSpan 向けに拡張されました。一方で、SpanRawSpan 自体には extracting() がまだ追加されておらず、UnsafeBufferPointer ファミリにも SE-0437 で取りこぼされたメソッドが残っていました。これは、non-escapable 型のスライスを返すのに必要な lifetime dependency の表記が当時はまだ確定していなかったためです。

その表記が固まったいま、Span / RawSpan をはじめとする一連の型でスライス操作の書き方が揃っていない状態を解消する必要があります。

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

MutableSpan で確立された extracting() メソッド群を、Span / RawSpan および UnsafeBufferPointer ファミリ全般に揃えて提供します。シグネチャは次の形に統一されます。

public func extracting(_ bounds: Range<Index>) -> Self
public func extracting(_ bounds: some RangeExpression<Index>) -> Self
public func extracting(_: UnboundedRange) -> Self
@unsafe public func extracting(unchecked bounds: Range<Index>) -> Self
@unsafe public func extracting(unchecked bounds: ClosedRange<Index>) -> Self

public func extracting(first maxLength: Int) -> Self
public func extracting(droppingLast k: Int) -> Self
public func extracting(last maxLength: Int) -> Self
public func extracting(droppingFirst k: Int) -> Self

対象となる標準ライブラリの型は以下のとおりです。すでに一部の extracting() を持っている型については、不足分が追加されてフルセットに揃えられます。

  • Span<T>
  • RawSpan
  • UnsafeBufferPointer<T> / UnsafeMutableBufferPointer<T>
  • Slice<UnsafeBufferPointer<T>> / Slice<UnsafeMutableBufferPointer<T>>
  • UnsafeRawBufferPointer / UnsafeMutableRawBufferPointer
  • Slice<UnsafeRawBufferPointer> / Slice<UnsafeMutableRawBufferPointer>

lifetime dependency

SpanRawSpan のような non-escapable 型の extracting() には、戻り値のライフタイムが元の値に紐付くことを示す lifetime dependency を表す属性が付きます。具体的には @_lifetime(copy self) が付与され、戻り値となる新しい Span などは元の self と同じライフタイム範囲でだけ有効です。escapable な型(UnsafeBufferPointer ファミリなど)にはこの属性は付きません。

let span: Span<Int> = ...
let head = span.extracting(first: 4) // span が生きている間だけ有効

使い方の例

範囲指定によるスライス、先頭・末尾の切り出し、先頭・末尾を落とした残りの取得が、subscript を使わずに同じスタイルで書けるようになります。

let middle = span.extracting(2..<6)
let head   = span.extracting(first: 4)
let tail   = span.extracting(last: 4)
let body   = span.extracting(droppingFirst: 1).extracting(droppingLast: 1)
let all    = span.extracting(...)

境界チェックを省略する unchecked 版は @unsafe 付きで、利用側に「範囲が妥当であることを呼び出し側で保証する責任がある」ことを伝えます。

let slice = span.extracting(unchecked: 0..<n) // n が範囲内であることを呼び出し側で保証

subscript などの誤用に対する誘導

Span / RawSpan / MutableSpan / MutableRawSpan には、Collection 由来のスライス用 subscriptdropFirst(_:) のような名前のメソッドを 使えない API として 追加し、対応する extracting() 呼び出しに誘導するヒントを出します。

@available(*, unavailable, renamed: "extracting(_ bounds:)")
public subscript(bounds: Range<Index>) -> Self { extracting(bounds) }

@available(*, unavailable, renamed: "extracting(first:)")
public func droppingFirst(_ k: Int) -> Self { extracting(first: k) }

これにより、Collection の感覚で span[0..<4]span.dropFirst(1) と書いても、コンパイラが「extracting(_ bounds:) / extracting(first:) を使ってください」と案内してくれます。

Future Directions(今後の見通し)

提案では今回のスコープ外として、ownership 種別による曖昧性解消の方向性が示されています。今回追加される extracting() はすべて borrowing です。MutableSpan には mutating 版もありますが、用途によっては consuming 版も欲しくなります。同名メソッドで borrowing / mutating / consuming の3種を区別するために、命名規則を整える方向と新しい構文を導入する方向のどちらを採るかは、今後の Proposal で別途検討するとされています。実現を約束するものではありません。