Standard Library Primitives for Nonescapable Types
01 何が問題だったのか
SE-0446 で non-escapable な型(~Escapable)が Swift に導入され、SE-0447 の Span のように「借用した連続メモリへのビュー」を型として表せるようになりました。しかし、標準ライブラリの基本的な汎用型はまだ 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 の取りこぼしとして、
ManagedBufferPointerのEquatable適合やUnsafe[Mutable]BufferPointer.indices、Slice上のバッファポインタ系操作がまだ noncopyable なElementに一般化されておらず、Span/InlineArrayとの整合も崩れていました。
言語側で non-escapable 型が書けるようになっても、こうした基盤 API が対応していないと、API 表面に Span のような借用型を露出させること自体が難しく、機能を十分に活用できませんでした。
02 どのように解決されるのか
標準ライブラリの基本部品を ~Escapable に一般化し、non-escapable 型を扱う上で最低限必要な道具を揃えます。同時に SE-0437 で取りこぼされた noncopyable 周りの穴も塞ぎます。
なお、non-escapable 値の寿命関係を関数シグネチャで明示的に表す構文はまだ確定していないため、map や init(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 の中でしか使えません。
値なし側は .none・nil リテラル・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() は self を nil にしつつ元の値を返し、返り値の寿命は元の 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 対応は別提案に委ねられます。
ManagedBufferPointerのEquatable適合がElement: ~Copyableでも成り立つようになります。比較はクラス参照の同一性ベースなので、要素のコピー可能性に依存しません。Unsafe[Mutable]BufferPointer.indicesがElement: ~Copyableでも使えるようになります。SpanやInlineArrayのindicesと揃い、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 の一般化(寿命記法が入った後)。