Borrowing and consuming pattern matching for noncopyable types
01 何が問題だったのか
SE-0390 で ~Copyable な struct / enum(non-copyable 型)を宣言できるようになりましたが、switch 文での扱いには大きな制約がありました。non-copyable な値に対する switch は常に consuming 扱いで、パターンマッチが走った時点で値は消費され、マッチ後には同じ値を再び使えなくなってしまうのです。
これはとくに non-copyable な enum の表現力を大きく損ねます。enum の関連値にアクセスする手段は switch のパターンマッチしかなく、それが常に値を消費してしまうと、たとえば「状態を確認したいだけ」「関連値を覗き見たいだけ」といった用途でも元の値を手放さざるを得ません。
struct NC: ~Copyable {}
enum NoncopyableEnum: ~Copyable {
case copyable(Int)
case noncopyable(NC)
}
var foo: NoncopyableEnum = ...
switch foo { // SE-0390 時点ではエラー。consume foo と明示しないと通らない
case .copyable(let n):
print(n)
case .noncopyable(let x):
// ...
}
// foo はこの switch で消費されてしまい、以降使えない
必要だったのは、non-copyable な値を借りたままパターンマッチできる仕組みと、パターンごとにどの所有権モード(借りるのか、消費するのか)で動くかを自然に決められるモデルです。
02 どのように解決されるのか
non-copyable な値に対する switch は、もはや常に consuming ではなく、対象式とパターンの組み合わせから所有権モードを推論するようになります。加えて、パターン中に borrowing なバインディングを明示的に書けるようにし、所有権の意図をパターン側から制御できるようにします。
switch の3つの所有権モード
switch の振る舞いは次の3つのいずれかに分類されます。右に行くほど厳しい(強い所有権を要求する)モードです。
- copying: 対象はセマンティクス上コピーされ、必要に応じてさらに複製も作られる。copyable な値に対するベースラインです。
- borrowing: 対象は
switchブロックの間だけ借りられる。 - consuming: 対象は
switchブロックで消費される。
コンパイラはパターンを再帰的に見て、より厳しい所有権を要求するパターンが現れるたびにモードを強めていきます。
ベースラインモードの決まり方
non-copyable な対象のベースラインは、対象式の形で決まります。
- 変数や stored property(および実験的な
_read/_modify/unsafeAddressアクセサを持つプロパティ・サブスクリプト)を参照していて、かつconsume演算子で明示的に消費していない場合は borrowing。 - それ以外(関数の戻り値などの一時値)は consuming。
copyable な型が baseline copying なのは従来と同じです。
パターンと所有権モードの例
copyable な enum の場合、let バインディングはすべて copying のままです。
enum CopyableEnum {
case foo(Int)
case bar(Int, String)
}
case let x: // copying
case .foo(let x): // copying
case .bar(let x, let y): // copying
non-copyable な enum の場合、ベースラインと各 let バインディングの型に応じてモードが決まります。
struct NC: ~Copyable {}
enum NoncopyableEnum: ~Copyable {
case copyable(Int)
case noncopyable(NC)
}
var foo: NoncopyableEnum // stored variable
switch foo {
case let x: // borrowing
case .copyable(let x): // borrowing(x: Int は copyable)
case .noncopyable(let x): // borrowing
}
func bar() -> NoncopyableEnum { ... } // 一時値を返す
switch bar() {
case let x: // consuming
case .copyable(let x): // borrowing(x: Int は copyable で、対象の一部だけ取り出せば済む)
case .noncopyable(let x): // consuming
}
SE-0390 時点では switch foo のような書き方はエラーでしたが、本提案では上記のとおり borrowing として正しく動作します。これまでのように switch consume foo と明示すれば、従来どおり consuming として振る舞わせることもできます。
マッチ中に対象を mutate / consume できない
switch のパターン評価順は未規定で、マッチが最終的に決まるまでは「どのケースが当たったか」が確定しません。non-copyable な型はコピーで逃げられないので、マッチが確定するまで対象を mutate したり消費したりすることはできません。
多くのパターン(enum ケースや、将来的に対応予定の non-copyable タプルなど)は、「条件を判定する部分」と「バインディングを作るために値を消費する部分」を分けて扱えるため、let / var バインディングを含んでいても問題なく動きます。
extension Handle {
var isReady: Bool { ... }
}
let x: MyNCEnum = ...
switch consume x {
// 複数ケースに `let y` があっても、マッチが確定してからバインディングのための
// 消費が行われるので OK
case .foo(let y) where y.isReady:
y.close()
case .foo(let y):
y.close()
}
一方、where 節ではバインディングを消費できません。where の評価後にマッチが失敗しうる以上、その時点で値を消費すると後続のケースに値が残らなくなるためです。case 本体では消費できます。
extension Handle {
consuming func tryClose() -> Bool { ... }
}
switch consume x {
// error: `where` 節では `y` を消費できない
case .foo(let y) where y.tryClose():
y.close()
case .foo(let y):
y.close() // 本体での消費は OK
}
同じ理由で、~= 演算子が対象を消費するような式パターンも non-copyable な対象には使えません。
extension Handle {
static func ~=(identifier: Int, handle: consuming Handle) -> Bool { ... }
}
switch consume x {
// error: マッチ確定前に対象を消費する `~=` は使えない
case .foo(42):
...
case .foo(let y):
...
}
なお、non-copyable 型はまだ動的キャストに対応していませんが、将来 is T / as T を受け入れるときも同じ方針になります。is T は borrowing のまま判定でき、as T は消費を伴うため consuming な switch に限られ、かつ p as T の中の p は irrefutable で where 節を持てない、といった制約が付く見込みです。
case 条件(if / while / for / guard)
if case <pattern> = <subject> などの case 条件は、「そのパターンだけを持ち、マッチ成功がバインディング付きの case、失敗が default に対応する switch」と考えれば同じ規則で動きます。対象への所有権の取り方も、そのパターンの要求に従います。
ソース互換性
SE-0390 は non-copyable 変数への switch に consume 演算子を要求していました。この書き方は引き続き動きます(ただし実際のマッチが borrowing で済むケースでも、バインディングの寿命は consume したタイミングで終わる点が従来の実装と若干異なります)。enum にはまだ deinit を書けず、non-copyable タプルも未対応、deinit を持つ struct は部分消費できず全体として消費するしかない、といった現状から、実挙動への影響はほぼ無いと見込まれています。
また、従来は理論上は consuming な ~= や where 節での消費が可能でしたが、本提案ではこれらを明確に禁止します。既存実装でもこれらを実用に乗せるのは難しかったため、実害は無いと判断されています。
Future Directions
将来の方向性としては次のようなアイデアが挙げられています(いずれも speculative で、実現を約束するものではありません)。
inoutパターン: パターンから排他参照を取って部分的にその場で mutate する、borrowing と consuming の中間の mutating なswitchモードです。letの自動借用: copyable な型でletの形式的コピーが最適化で借用に変わるのと同じ発想を non-copyable なletバインディングにも広げ、「消費が必要でなければ自動で borrow」という「do what I mean」的な挙動にする方向です。合わせて、明示的なconsumingバインディングをパターンに書けるようにする案もあります。enumのdeinit: borrowingswitchが可能になったことで、enumにもdeinitを許す素地ができました。ただし consumingswitchで分解するとdeinitを迂回してしまうため、deinitを持つenumは consuming な分解を禁止する、といった制約付きになる見込みです。- 明示的な
borrow演算子:switch borrow xのように、copyable な値や一時値でも明示的に借用としてswitchできるようにする案です。 - パターン中の
borrowingバインディング: 関数のローカル束縛や nonescapable 型のフィールドとしてのborrowing/inoutバインディングが導入された後に、パターン中でも明示的なborrowingバインディングを書けるようにし、copyable なバインディングの暗黙コピーを抑える、といった用途が考えられています。