Swift Digest
SE-0465 | Swift Evolution

Standard Library Primitives for Nonescapable Types

Proposal
SE-0465
Authors
Karoy Lorentey
Review Manager
Doug Gregor
Status
Implemented (Swift 6.2)

01 何が問題だったのか

SE-0446 で non-escapable な型(~Escapable)が Swift に導入され、SE-0447Span のように「借用した連続メモリへのビュー」を型として表せるようになりました。しかし、標準ライブラリの基本的な汎用型はまだ escapable な値しか受け取れず、non-escapable な値を扱う API を書こうとすると素朴な道具が足りない状態でした。

  • Optional<Wrapped>Wrapped が escapable であることを前提にしていて、Span<Int>? のような「値があるかもしれないし無いかもしれない Span」を素直に書けませんでした。
  • 同様に Result<Success, Failure>Success が non-escapable な場合を扱えず、Result<Span<Int>, some Error> を返す関数を書けませんでした。
  • MemoryLayout<T>T のサイズ・ストライド・アラインメントを問い合わせたくても、T が non-escapable なら型パラメータに置けませんでした。
  • 寿命を人工的に延ばす withExtendedLifetime(_:_:) も escapable な入力しか受け付けず、Span を使うコードでは選択肢になりませんでした。また、実運用では defer { withExtendedLifetime(obj) {} } のように空クロージャで呼ぶ形が定着しており、クロージャを必ず取る API が実態と合っていないという別の問題も抱えていました。
  • ObjectIdentifier によるメタタイプ識別子の生成や、メタタイプ同士の == / != 比較も、Copyable & Escapable なメタタイプに限られていました。
  • SE-0437 の取りこぼしとして、ManagedBufferPointerEquatable 適合や Unsafe[Mutable]BufferPointer.indicesSlice 上のバッファポインタ系操作がまだ noncopyable な Element に一般化されておらず、Span / InlineArray との整合も崩れていました。

言語側で non-escapable 型が書けるようになっても、こうした基盤 API が対応していないと、API 表面に Span のような借用型を露出させること自体が難しく、機能を十分に活用できませんでした。

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

標準ライブラリの基本部品を ~Escapable に一般化し、non-escapable 型を扱う上で最低限必要な道具を揃えます。同時に SE-0437 で取りこぼされた noncopyable 周りの穴も塞ぎます。

なお、non-escapable 値の寿命関係を関数シグネチャで明示的に表す構文はまだ確定していないため、mapinit(catching:)?? のように「結果の寿命を入力から精密に導かなければならない」高階 API の一般化は将来の提案に持ち越されます。

non-escapable な Optional

Optional を次のように条件付きで escapable な型に一般化します。

enum Optional<Wrapped: ~Copyable & ~Escapable>: ~Copyable, ~Escapable {
  case none
  case some(Wrapped)
}

extension Optional: Copyable where Wrapped: Copyable & ~Escapable {}
extension Optional: Escapable where Wrapped: Escapable & ~Copyable {}
extension Optional: BitwiseCopyable where Wrapped: BitwiseCopyable & ~Escapable {}
extension Optional: Sendable where Wrapped: ~Copyable & ~Escapable & Sendable {}

Wrapped が non-escapable なら Optional も non-escapable になり、包んでいる値と同じ寿命制約を受けます。Span<Int>.some に包んでも、外側に持ち出せるようになるわけではありません。

値あり側は、ファクトリ呼び出し・暗黙の optional promotion・イニシャライザのどれを使っても、寿命が元の値からそのままコピーされます。

func sample(_ span: Span<Int>) {
  let a = Optional.some(span) // 明示的なケースファクトリ
  let b: Optional = span      // 暗黙の optional promotion
  let c = Optional(span)      // 明示的なイニシャライザ
}

a / b / c はどれも元の span と同じく sample の中でしか使えません。

値なし側は .nonenil リテラル・var の暗黙初期化のいずれも使えます。

func sample(_ span: Span<Int>) {
  var d: Span<Int>? = .none
  var e: Span<Int>? = nil
  var f: Span<Int>?
}

nil は特別で、寿命制約を持たない「immortal」な値として扱われます。そのためローカル変数では、nil ↔ 具体的な寿命を持つ値、の間で自由に再代入でき、寿命を「狭めたり広げたり」できます(グローバル変数やカスタム構造体の stored property では、将来的にもっと厳しい規則が課される想定です)。

func sample(_ span: Span<Int>) {
  var maybe: Span<Int>? = nil // immortal
  maybe = span                // 具体的な寿命
  maybe = nil                 // 再び immortal
}

nil リテラルを受け付けるため、ExpressibleByNilLiteral~Copyable & ~Escapable に一般化されます。init(nilLiteral:) は暗黙に immortal な値を返す扱いです。

アンラップ系は既存の構文がそのまま使えます。switch / if case / guard case!? によるオプショナルチェイン、if let / guard let のいずれも、アンラップ結果の寿命は元の Optional と同じになります。

func count(of maybeSpan: Span<Int>?) -> Int {
  switch maybeSpan {
  case .none: return 0
  case .some(let span): return span.count
  }
}

func count2(of maybeSpan: Span<Int>?) -> Int {
  guard let span = maybeSpan else { return 0 }
  return span.count
}

標準ライブラリが定義する nil との比較(== / != / ~=)、take()unsafelyUnwrapped も non-escapable ケースに拡張されます。take()selfnil にしつつ元の値を返し、返り値の寿命は元の Optional と同じです。

extension Optional where Wrapped: ~Copyable & ~Escapable {
  mutating func take() -> Self
}

extension Optional where Wrapped: ~Escapable {
  var unsafelyUnwrapped: Wrapped { get }
}

一方で ?? は「左辺の寿命」と「右辺(autoclosure)の結果の寿命」の交わりを返す必要があるため、map / flatMap などと合わせて今回は据え置きです。

non-escapable な Result

Result は success 側を non-escapable にできるように一般化します。

enum Result<Success: ~Copyable & ~Escapable, Failure: Error> {
  case success(Success)
  case failure(Failure)
}

extension Result: Copyable where Success: Copyable & ~Escapable {}
extension Result: Escapable where Success: Escapable & ~Copyable {}
extension Result: Sendable where Success: Sendable & ~Copyable & ~Escapable {}

Result.init(catching:)Result.map のように寿命の合成が絡むものは先送りされますが、寿命的に素直な get()mapError(_:) は一般化されます。

extension Result where Success: ~Copyable & ~Escapable {
  consuming func get() throws(Failure) -> Success
  consuming func mapError<NewFailure>(
    _ transform: (Failure) -> NewFailure
  ) -> Result<Success, NewFailure>
}

func sample<E: Error>(_ res: Result<Span<Int>, E>) -> Int {
  guard let span = try? res.get() else { return 42 }
  return 3 * span.count + 9
}

get() で取り出した値の寿命は元の Result と同じです。

MemoryLayout の一般化

non-escapable 型にもメモリレイアウトは定義できるので、MemoryLayout~Escapable に広げて size / stride / alignment を問い合わせられるようにします。

enum MemoryLayout<T: ~Copyable & ~Escapable>: ~BitwiseCopyable, Copyable, Escapable {}

extension MemoryLayout where T: ~Copyable & ~Escapable {
  static var size: Int { get }
  static var stride: Int { get }
  static var alignment: Int { get }
  static func size(ofValue value: borrowing T) -> Int
  static func stride(ofValue value: borrowing T) -> Int
  static func alignment(ofValue value: borrowing T) -> Int
}

print(MemoryLayout<Span<Int>>.size)      // 例: 16
print(MemoryLayout<Span<Int>>.stride)    // 例: 16
print(MemoryLayout<Span<Int>>.alignment) // 例: 8

UnsafePointer などのポインタ型自体を non-escapable pointee 対応に広げるのは、寿命セマンティクスの設計が必要なため別の提案に回されます。

寿命管理と新しい extendLifetime(_:)

withExtendedLifetime~Copyable & ~Escapable な入力にも対応するように一般化されます(結果型はまだ escapable 限定)。

func withExtendedLifetime<
  T: ~Copyable & ~Escapable, E: Error, Result: ~Copyable
>(
  _ x: borrowing T,
  _ body: () throws(E) -> Result
) throws(E) -> Result

func withExtendedLifetime<
  T: ~Copyable & ~Escapable, E: Error, Result: ~Copyable
>(
  _ x: borrowing T,
  _ body: (borrowing T) throws(E) -> Result
) throws(E) -> Result

加えて、defer と組み合わせて「ただ寿命を延ばしたい」用途向けに、クロージャを取らない関数が追加されます。

func extendLifetime<T: ~Copyable & ~Escapable>(_ x: borrowing T)

これにより、従来は次のように書いていたコードを、

weak var ref = obj
defer { withExtendedLifetime(obj) {} }
foo(ref!)

シンプルに書き直せます。

weak var ref = obj
defer { extendLifetime(obj) }
foo(ref!)

既存のクロージャ版は deprecate されず共存します。

メタタイプ比較と ObjectIdentifier

メタタイプに対する == / !=~Copyable & ~Escapable なメタタイプも受け付けるようになります。

func == (
  t0: (any (~Copyable & ~Escapable).Type)?,
  t1: (any (~Copyable & ~Escapable).Type)?
) -> Bool
func != (
  t0: (any (~Copyable & ~Escapable).Type)?,
  t1: (any (~Copyable & ~Escapable).Type)?
) -> Bool

print(Atomic<Int>.self == Span<Int>.self) // false

ObjectIdentifier の型識別子生成も同様に一般化され、noncopyable / non-escapable なメタタイプから ObjectIdentifier を作れます。生成された ObjectIdentifier 自身は従来通り copyable かつ escapable で、Hashable / Comparable として普通に使えます。

let id3 = ObjectIdentifier(Atomic<Int>.self)  // noncopyable
let id4 = ObjectIdentifier(Span<Int>.self)    // non-escapable

SE-0437 の取りこぼしの補修

以下は noncopyable 側の補修で、non-escapable 対応は別提案に委ねられます。

  • ManagedBufferPointerEquatable 適合が Element: ~Copyable でも成り立つようになります。比較はクラス参照の同一性ベースなので、要素のコピー可能性に依存しません。
  • Unsafe[Mutable]BufferPointer.indicesElement: ~Copyable でも使えるようになります。SpanInlineArrayindices と揃い、for i in buf.indices { ... } が要素型に関わらず書けます(noncopyable コンテナ向けの新しい反復モデルが来るまでのつなぎでもあります)。
  • Slice 上のバッファポインタ系操作 — moveInitializeMemory(as:fromContentsOf:)bindMemory(to:)withMemoryRebound(to:_:)assumingMemoryBound(to:) — が、対応するバッファポインタ本体と同じく T: ~Copyable まで一般化されます。Slice 自身はまだ Element: Copyable が必要です。

Future Directions

今回はあくまで「non-escapable 型を API 表面に出せるようにする最小限の下地」です。いずれも speculative で、実現を約束するものではありません。

  • 寿命依存関係を表す安定した構文の導入、および寿命を明示しない関数の既定セマンティクスの定義。
  • 寿命依存関係を unsafe に上書きする機構、および non-escapable 型との unsafe なビットキャスト。
  • ポインタ型(UnsafePointer / UnsafeBufferPointer 族、場合によっては ManagedBuffer)の non-escapable pointee 対応。これが入れば MemoryLayout 一般化の実用価値も一気に上がります。
  • non-escapable 要素を扱うジェネリックコンテナと、その反復モデル。Span 自身を non-escapable 要素対応に広げる拡張も含みます。
  • 既存の標準プロトコル(Equatable など)の non-escapable / noncopyable 一般化。ABI 互換性を壊さずに進める必要があるため、複数の提案に分けて進むことが想定されています。
  • 今回先送りされた Optional.map / Result.init(catching:) / ?? などの高階 API の一般化(寿命記法が入った後)。