Optional の non-copyable 対応の改善と一般化
Optional noncopyable improvements and generalizations
このダイジェストはClaude Opus 4.7 / 4.8によって生成されたものです(License)。原文はこちら↗。
01 何が問題だったのか
SE-0437 によって標準ライブラリの Optional は non-copyable な値も包めるようになりましたが、実際に non-copyable な Optional を扱おうとすると、用途の幅に対して API が足りておらず、書きにくさが目立ちました。
中身を覗き見るだけでも Optional が consume される
Optional を if let で取り出す既存の構文は、Optional の値を consume する形で動きます。Copyable な要素なら問題になりませんが、non-copyable な要素が入っていると、if let した時点で元の Optional を二度と使えなくなります。
if let payload = optional {
// ...
}
foo(optional) // error: use after consume!
switch 文ベースの非破壊なパターンマッチ(SE-0432)を使えば borrow したまま中身を取り出せますが、if let / guard let に慣れた目には冗長で、書き慣れないと書きづらい記述になります。
switch optional {
case .some(let wrapped):
// wrapped は borrow されている
default:
break
}
foo(optional) // ok
中身を非破壊に mutate する手段がない
Optional の payload を「consume せずに inout で渡したい」ケースでも、満足のいく書き方がありません。簡単なプロパティ更新や mutating メソッド呼び出しなら ?. の chaining で済みますが、payload を inout で関数に渡したいときは ! を強制する書き方になり、
if someStruct.x != nil {
foo(&someStruct.x!)
}
あるいは switch で一度 consume してから再代入する、非常に冗長なコードを書く必要がありました。
func bar(_ x: inout NoncopyableString?) {
switch consume x {
case .some(var string):
foo(&string)
x = consume string
default:
x = nil
}
}
map / flatMap / unsafelyUnwrapped が non-copyable に未対応
SE-0437 では Optional 自体の Copyable 条件は外されたものの、map / flatMap / unsafelyUnwrapped といった日常的に使う API はそのままで、payload が non-copyable / non-escapable だと使えません。そのため、map / flatMap を使えば1行で書けたコードを、手書きの switch で書き直す必要が出ていました。
cache.opt!.append(...) のような不要な強制アンラップ
「いま値を入れた直後の Optional をすぐに使う」ようなパターンでは、コードを読む人間にはアンラップ不要なことが明らかでも、
cache.opt = items
// ...
cache.opt!.append(newItem)
! を書かないとアクセスできず、cache の中身が外から見えない関数を挟んだ場合などには、! がコンパイラの最適化でも消し切れずに残ってしまうことがありました。
02 どのように解決されるのか
Optional に、payload を borrow / mutate / 挿入するための新しいメソッドを追加し、合わせて map / flatMap / unsafelyUnwrapped を non-copyable / non-escapable 対応に一般化します。borrow() と mutate() は payload への参照を Ref<Wrapped> / MutableRef<Wrapped> として返すため、これらの可用性は Ref / MutableRef の可用性に合わせて決まります。
borrow(): payload を非消費に借りる
extension Optional where Wrapped: ~Copyable & ~Escapable {
@lifetime(borrow self)
public func borrow() -> Ref<Wrapped>?
}
borrow() は borrowing Optional<T> から呼び出せ、payload があれば Ref<Wrapped> に包んだ「借り物の参照」を Optional として返します。これを if let と組み合わせれば、元の Optional を消費せずに payload を覗き見ることができます。
func bar(_ x: borrowing SomeNoncopyable) { /* ... */ }
if let payload = optional.borrow() {
bar(payload.value) // borrowing Wrapped として渡せる
}
foo(optional) // ok
mutate(): payload を inout で借りる
extension Optional where Wrapped: ~Copyable {
@lifetime(&self)
public mutating func mutate() -> MutableRef<Wrapped>?
}
mutate() は inout Optional<T> から呼び出して、payload を MutableRef<Wrapped> として返します。if var と組み合わせれば、payload を inout で別関数に渡すパターンも自然に書けるようになります。
func baz(_ x: inout SomeNoncopyable) { /* ... */ }
if var payload = optional.mutate() {
baz(&payload.value) // payload を直接 mutate できる
}
foo(optional) // ok
insert(): 値を入れつつ参照を返す
extension Optional where Wrapped: ~Copyable {
@lifetime(&self)
public mutating func insert(_ new: consuming Wrapped) -> MutableRef<Wrapped>
}
insert(_:) は、Optional に新しい値を入れた上で、その新しい payload への mutable な参照を返します。元の Optional に値が入っていた場合は、その値を破棄してから新しい値で置き換えます。
struct Cache: ~Copyable {
var opt: UniqueArray<Int>?
}
var cache = Cache()
let items: UniqueArray<Int> = fooBar()
var itemsRef = cache.opt.insert(items)
// ...
let newItem = await retrieveNewItem()
itemsRef.value.append(newItem) // `!` でアンラップしなくてよい
! を避けることでコードの意図が明確になるだけでなく、! が最適化で消えなかったケースで生じていたランタイムチェックも省けます。
map / flatMap の一般化(常に consuming)
extension Optional where Wrapped: ~Copyable & ~Escapable {
@lifetime(copy self)
public consuming func map<Result: ~Copyable & ~Escapable, E: Error>(
_ transform: (consuming Wrapped) throws(E) -> Result
) throws(E) -> Result?
@lifetime(copy self)
public consuming func flatMap<Result: ~Copyable & ~Escapable, E: Error>(
_ transform: (consuming Wrapped) throws(E) -> Result?
) throws(E) -> Result?
}
non-copyable な payload に対する map / flatMap には、self を consume するもの、borrow するもの、mutate するものの3通りが理論的にはありえますが、Swift には現状この3つを map という同名で区別する手段がありません。そこで本提案では、一般化版は 常に self を consume する 形で1本に絞られています。borrow したい場合や mutate したい場合は、先ほどの borrow() / mutate() を挟んでから map を呼ぶことになります。
func foo(x: consuming Optional<UniqueArray<String>>) -> Optional<String> {
x.map { $0[0] } // x を consume する map
}
func bar(x: borrowing Optional<Atomic<Int>>) -> Optional<Int> {
x.borrow().map { // Optional<Ref<Atomic<Int>>> に対する map
$0.value.load(ordering: .relaxed) &+ 1
}
}
func baz(x: inout Optional<UniqueArray<Int>>) -> Optional<Int> {
x.mutate().map { // Optional<MutableRef<UniqueArray<Int>>> に対する map
$0.value.append(123)
return $0.value.count
}
}
既存の Wrapped: Copyable 版の map / flatMap は引き続き残り、より具体的なオーバーロードとして優先されるので、既存のコードの挙動は変わりません。
unsafelyUnwrapped の一般化
extension Optional where Wrapped: ~Copyable & ~Escapable {
public var unsafelyUnwrapped: Wrapped {
consuming get
}
}
unsafelyUnwrapped も non-copyable / non-escapable な payload に対応します。consuming get なので、呼び出すと Optional 自体が消費されます。
let optMutex: Optional<Mutex<Int>> = ...
let mutex: Mutex<Int> = optMutex.unsafelyUnwrapped
optMutex?.withLock { ... } // error: use of 'optMutex' after consume
03 今後の見通し
提案では次のような発展方向が挙げられています。いずれも将来の構想として示されているもので、実現を約束するものではありません。
if borrow / if inout 構文による直接的なスコープ束縛
borrow() / mutate() の代わりに、コンパイラ側で if borrow x = optional / if inout x = optional のような構文を解釈し、Optional の payload を borrow / mutate のスコープに直接束縛できるようにする案が示されています。
if borrow x = optional {
// x は payload への borrow 参照
}
foo(optional) // ok
この方向に進む場合、map / flatMap を「常に consuming」に固定した今回の選択を再検討する必要が出てきます。たとえば consumingMap / borrowingMap / mutatingMap のように分けることになりますが、API が増えてしまうため好ましい解にはなりにくいと注釈されています。
Optional: Equatable / Optional: Hashable の一般化
SE-0499 で Equatable / Hashable などのプロトコルが non-copyable / non-escapable 対応に一般化されたため、Optional の Equatable / Hashable 適合も合わせて一般化したいところです。ただし、ABI が安定している古い OS で == / hash(into:) の汎用版が呼ばれた際に payload を誤ってコピーしてしまうことを防ぐ仕組みが現状ないため、すぐには進められない、と整理されています。
Ref / MutableRef の自動 dereference
borrow() / mutate() の結果を map でつなぐとき、現状ではクロージャ内で $0.value のように一段経由する必要があります。Ref / MutableRef に特別な扱いを与えてプロパティ・メソッド呼び出しを自動的に payload に振り向けられれば、もっと自然に書けるようになります。
func bar(x: borrowing Optional<Atomic<Int>>) -> Optional<Int> {
x.borrow().map { // `.value.` 不要
$0.load(ordering: .relaxed) &+ 1
}
}
将来的には Ref / MutableRef 専用ではなく、Rust の Deref のような汎用プロトコルとしてこの仕組みを抽象化する案も示されており、UniqueBox のような型でも恩恵を受けることが想定されています。