noncopyable valueの部分消費
Partial consumption of noncopyable values
このダイジェストはClaude Opus 4.7 / 4.8によって生成されたものです(License)。原文はこちら↗。
01 何が問題だったのか
SE-0390 でnon-copyableな struct / enum が導入されましたが、non-copyableな集約(aggregate)の個々のフィールドを操作するのが困難でした。Swift ではnon-copyableな値はパス上で高々1回しか consume できないという制約があり、この「1回」はフィールドごとではなく値全体として数えられていたためです。
たとえば、二つのnon-copyableなフィールドを持つ Pair を考えます。
struct Unique: ~Copyable {}
struct Pair: ~Copyable {
let first: Unique
let second: Unique
}
この Pair について、中身を入れ替えた新しい Pair を返すだけの関数ですら、素直には書けませんでした。
extension Pair {
consuming func swap() -> Pair {
return Pair(
first: second, // error: cannot partially consume 'self'
second: first // error: cannot partially consume 'self'
)
}
}
first を読み出した時点で self を一度 consume したことになり、続けて second を読み出すと「同じ値を2回 consume している」と扱われてしまいます。copyableな値では当たり前に書けるこの種のパターンが、non-copyable型では回避策に頼らざるを得ませんでした。
同様に、deinit の中でさえ各フィールドを個別に手放せないため、自身が所有するリソースを deinit で開放するような自然な書き方も難しい状況でした。
02 どのように解決されるのか
「non-copyableな値はパス上で高々1回 consume できる」というルールを、各non-copyableフィールドがパス上で高々1回 consume される というルールに緩和します。これにより、deinit を持たないnon-copyable集約については、フィールドごとに個別に consume する 部分consume(partial consumption) が可能になります。
func takeUnique(_ elt: consuming Unique) {}
extension Pair {
consuming func swap() -> Pair {
return Pair(
first: second, // OK
second: first // OK
)
}
}
すべての分岐ですべてのフィールドを consume する必要はありません。あるパスでは first のみ、別のパスでは second のみを consume するといった使い方もできます。
extension Pair {
consuming func passUnique(_ front: Bool) {
if front {
takeUnique(first)
// second はここで破棄される
} else {
takeUnique(second)
// first はここで破棄される
}
}
}
フィールドの寿命の延長
あるパスでフィールドが consume されていない場合、そのフィールドの破棄は可能な限り遅らせて行われます。上の例で、first を consume しなかった分岐の末尾で first が自動的に破棄されるのはそのためです。
if/else の後ろでまとめて破棄することはできません。合流地点で破棄するには「どちらの分岐でも値が残っている」という状態をコピーで実現する必要があり、non-copyable型ではそれが許されないからです。
明示的な consume
consume キーワードを使うと、フィールドを明示的にその時点で consume でき、寿命の自動延長を上書きできます。たとえば「first の破棄は必ず second より前にしたい」という場合は次のように書けます。
extension Pair {
consuming func passUnique(_ front: Bool) {
if front {
takeUnique(first)
// second はここで破棄される
} else {
_ = consume first
takeUnique(second)
}
}
}
copyableなフィールドの扱い
non-copyable集約に含まれる copyable なフィールドは、従来どおり何度でも consume できます。部分consumeが絡んでも、この性質は変わりません。
func takeString(_ name: consuming String) {}
struct Named: ~Copyable {
let unique: Unique
let name: String
consuming func unpack() {
takeString(name)
takeString(name)
takeUnique(unique)
takeString(name)
takeString(name)
}
}
別モジュールの型は @frozen 必須
非copyableな型が他のモジュールで public / package として定義されている場合、部分consumeは @frozen な型に限って許可されます。モジュール側でフィールドが追加・削除されたり、stored property が computed property に変わったりすると、ある版で成り立っていた部分consumeが別の版では成り立たなくなる可能性があるためです。@frozen はレイアウトを将来も変えないことの約束なので、そのような心配がありません。
このルールは library evolution 下のライブラリには必須のものですが、「ビルドモードによって言語規則が変わる」という事態を避けるため、library evolution を使わない場合にも一律に適用されます。
deinit を持つ型は deinit 内でのみ部分consume可能
deinit を持つnon-copyable型を外から部分consumeするのは許されません。部分的に consume された値に対して deinit を走らせるとフィールドの一部がすでに存在しないため、整合が取れなくなるためです。deinit を抑制する仕組みは現時点では提案されておらず、discard は将来の検討課題として残されています。
ただし、deinit の内部に限っては自分自身を部分consumeできます。これにより、non-copyableな型が所有するリソースを deinit で個別に手放す、という自然なパターンが書けます。
struct Pair2: ~Copyable {
let first: Unique
let second: Unique
deinit {
takeUnique(first) // self を部分consume
takeUnique(second) // self を部分consume
}
}
03 今後の見通し
今回のスコープ外として、次のような発展の方向性が示されています。いずれも将来の構想であり、実現を約束するものではありません。
deinit を持つ型の部分consume
本提案では、deinit を持つnon-copyable型を外から部分consumeすることは禁止されています。これは、部分的に consume された値に対して deinit を走らせるとフィールドの一部がすでに存在せず整合が取れなくなるためです。
将来的には、部分consumeの前に discard self を書いて deinit を走らせないことを明示する形で、この制限を緩和する案が示されています。
struct Box: ~Copyable {
var unique: Unique
deinit {...}
consuming func unpack() -> Unique {
discard self
return unique
}
}
部分的な再初期化
本提案ではフィールドを個別に consume できるようにするだけで、consume 後にフィールドを 再初期化 して集約を再び正当な状態に戻すことはできません。これが許されれば、copyable型では当たり前にできる次のような書き方を、non-copyable型でも書けるようになります。
struct Unique: ~Copyable {}
struct Pair: ~Copyable {
var first: Unique
var second: Unique
}
extension Pair {
mutating func swap() {
let tmp = first
first = second
second = tmp
}
}
copyableなフィールドの明示的なconsume
本提案はnon-copyable集約の non-copyableなフィールド を個別に consume することのみを対象としています。将来的には、copyable集約に含まれる copyable なフィールドを consume キーワードで明示的に consume できるようにする案が示されています。これにより、copyable なフィールドの寿命を終わらせる地点を明示的に指定できるようになります。
class C {}
func takeC(_ c: consuming C)
struct PairPlusC: ~Copyable {
let first: Unique
let second: Unique
let c: C
}
func disaggregate(_ p: consuming PairPlusC) {
takeUnique(p.first)
takeC(consume p.c) // p.c の寿命がここで終わる
takeUnique(p.second)
}
copyable集約そのものの部分consume
本提案はあくまでnon-copyable集約を対象としていますが、これを copyable 集約にも自然に拡張する方向性も示されています。
class C {}
struct CopyablePairOfCs {
let c1: C
let c2: C
}
func tearDownInOrder(_ p: consuming CopyablePairOfCs) {
takeC(consume p.c2)
takeC(consume p.c1)
}