Nonescapable Types メモ
PR: https://github.com/swiftlang/swift-evolution/pull/2565
~Copyable
と同様に ~Escapable
(@escaping できない)ような制限をもうけられるようにする?
Accept された
Proposal の DeepL 翻訳:
回避不可能な型
- 提案: SE-0446
- 作者: Andrew Trick, Tim Kientzle
- レビューマネージャー: Joe Groff
- ステータス: Accepted
- ロードマップ:BufferView 言語要件
- 実装:
main
ブランチで実装済み - 今後の機能フラグ:
NonescapableTypes
- レビュー: (pitch)
- 決定メモ: Acceptance
導入
局所的にコピーは可能だが、直接のコンテキスト外への代入や転送はできない型に対して、新しい型制約 ~Escapable
を追加することを提案します。
これは、SE-0390 で追加された ~Copyable
型を補完するもので、安全で高性能な API で使用できる、コンパイル時に強制される別の寿命管理セットを導入します。
さらに、これらの型は、他の型に格納されたデータへのポインタを安全に保持できるようにする寿命依存制約(将来の提案で追跡中)をサポートします。
この機能は、提案されている Span
ファミリー型にとって重要な要件です。
関連項目
- SE-0390: コピーできない構造体と列挙型
- バッファビューの言語サポート
- Swiftのパフォーマンス予測可能性を向上させるロードマップ:ARCの改善と所有権の制御
- 所有権に関するマニフェスト
- ドラフト・スパン提案
- ドラフト・ライフタイム依存性注釈提案
動機
Swiftの現在の「イテレータ」の概念には、極端にパフォーマンスが制限された環境で使用しようとした場合に明らかになるいくつかの弱点があります。
これらの弱点は、安全性を確保しながら、同時にマルチイテレータアルゴリズムをサポートするためにイテレータ値を任意にコピーできるようにしたいという要望から生じています。
例えば、Array用の標準ライブラリイテレータは、初期化時にArrayのコピーを論理的に作成します。これにより、配列に対する変更が反復処理に影響を及ぼすことがないようにします。
これは、イテレータがアクティブな間はストレージが解放されないように、イテレータが配列ストレージへの参照カウント付きポインタを格納することで実装されています。
これらの安全確認はすべて実行時のオーバーヘッドとなります。
さらに、実行時の正確性を確保するために参照カウントを使用すると、この種の型は制約の厳しい組み込み環境では使用できなくなります。
提案されている解決策
現在、「エスケープ可能」という概念は、Swift言語ではクロージャの機能として存在しています。
エスケープ不可能なクロージャは、非常に効率的なスタックベースの表現を使用できますが、
@escapingを指定したクロージャは、キャプチャした値をヒープ上に格納します。
Swift開発者がさまざまな型をnonescapableとしてマークできるようにすることで、次のような特定の使用制限のセットを適用するメカニズムを提供します。
- コンパイラによって自動的に検証できる。実際、Swiftコンパイラはすでに、エスケープ可能性という概念を内部で頻繁に使用しています。
- 厳格な使用制限を課すことで、高いパフォーマンスを実現できる。エスケープしない値はより効率的に管理できるため、コンパイラはまさにこの概念を使用しています。
- これらの型の一般的な使用を妨げない。
例えば、イテレータ型が非エスケープ可能とマークされていた場合、その型の利用者が効率的な動作を制限する可能性のある方法で値のコピーや保存を試みると、コンパイラは常にエラーメッセージを表示します。
これらのチェックは、ほとんどの場合、単一のローカルコンテキストで作成、使用、破棄されるイテレータの有用性を大幅に低下させるものではありません。
また、これらのチェックは、複数のイテレータを使用する場合でもローカルコピーを許可し、それらのコピーにも同じ制約を適用します。
別の提案では、ライブラリの作成者がイテレータの寿命を生成したオブジェクトに結びつける追加の制約を課すことを許可することで、安全性をさらに向上させる方法を示します。
これらの「寿命依存」制約は、コンパイル時に検証することもでき、イテレータのソースが変更されないこと、およびイテレータが特にソースよりも長生きしないことを保証します。
注意:ここでは、検討中の問題を説明するためにイテレータを使用しています。
現時点では、Swiftの現在のIteratorProtocol
プロトコルに対する変更を提案するものではありません。
詳細設計
新しい抑制可能な概念
新しい抑制可能なプロトコル Escapable
を標準ライブラリに追加し、現在の Swift のすべての型(唯一の例外は、非抑制可能なクロージャ)に暗黙的に適用します。
Escapable
型はグローバル変数に代入でき、任意の関数に渡したり、現在の関数やクロージャから返したりすることができます。
これは、この提案以前のすべての Swift 型の既存のセマンティクスと一致します。
具体的には、標準ライブラリに以下の宣言を追加します。
// エスケープ可能な型は、コピー可能である場合も、そうでない場合もあります
プロトコル Escapable: ~Copyable {}
~Escapable
はエスケープ不可を示します
具体的なコンテキストでは、~Copyableと
Copyable で使用したのと同じアプローチを使用して、
~Escapableを使用して型の
Escapable` 準拠を抑制します。
// 例:エスケープ不可な型
struct NotEscapable: ~Escapable {
...
}
エスケープ不可な値は、ローカルコンテキストからエスケープすることはできません。
- より大きなスコープのバインディングに割り当てることはできません
- 現在のスコープから返すことはできません
// 例:~Escapable型の基本的な制限
func f() -> NotEscapable {
let ne = NotEscapable()
borrowingFunc(ne) // 借用関数に渡すのはOK
let another = ne // ローカルコピーを作成するのはOK
globalVar = ne // 🛑 ~Escapable型をグローバル変数に代入するのは不可
return ne // 🛑 ~Escapable型を返すのは不可
}
注意:
セクション「[「返されたエスケープ不可能な値にはライフタイム依存が必要」]」(#Returns)では、初期化子を記述する方法について説明しています。
~Escapableがなければ、すべての型のデフォルトはエスケープ可能になります。
~Escapable` は機能を抑制するので、拡張機能で宣言することはできません。
// 例:デフォルトでエスケープ可能
struct Ordinary { }
extension Ordinary: ~Escapable // 🛑 拡張機能では機能の削除はできません
クラスを ~Escapable
と宣言することはできません。
~Escapable
はデフォルトのエスケープ可能要件を無効にします
一般的なコンテキストでは、ジェネリックコンテキストで使用される場合、~Escapable
により、エスケープ可能であるかもしれないしそうでないかもしれない値を扱う関数や型を定義することができます。
つまり、~Escapable
はデフォルトのエスケープ可能要件が抑制されていることを示します。
値がエスケープ可能でない可能性があるため、コンパイラは値のエスケープを保守的に防止する必要があります。
func f<MaybeEscapable: ~Escapable>(_ value: MaybeEscapable) {
// `value` は Escapable であるかもしれないし、そうでないかもしれない
globalVar = value // 🛑 可能性としてエスケープ不可能な型をグローバル変数に代入することはできない
}
f(NotEscapable()) // エスケープ不可能な引数で呼び出すのは問題ない
f(7) // エスケープ可能な引数で呼び出すのは問題ない
SE-0427 Noncopyable Generics には、
Escapable` などの抑制可能なプロトコルがジェネリック型システムでどのように処理されるかについて、より詳細な説明があります。
注意: Copyable
と Escapable
との間には関係はありません。
Copyable または noncopyable な型は escapable または nonescapable になることができます。
nonescapable なローカル変数に対する制約
nonescapable な値は、現在のスコープを越えて値が存続しないことが保証される限り、自由にコピーしたり、async や throw を含む他の関数に渡したりすることができます。
// 例: ノンエスケープ可能な型を持つローカル変数
func borrowingFunc(_: borrowing NotEscapable) { ... }
func consumingFunc(_: consuming NotEscapable) { ... }
func inoutFunc(_: inout NotEscapable) { ... }
func asyncBorrowingFunc(_: borrowing NotEscapable) async -> ResultType { ... }
func f() {
var value: NotEscapable
let copy = value // コピーはOK。ただし、コピーがグローバルから抜け出さない場合
globalVar = value // 🛑 グローバルへの代入は不可
SomeType.staticVar = value // 🛑 静的変数への代入は不可
async let r = asyncBorrowingFunc(value) // 借用を渡すのはOK
borrowingFunc(value) // 借用を渡すのはOK
inoutFunc(&value) // inoutを渡すのはOK
consumingFunc(value) // 消費を渡すのはOK
// `value` は上記で消費されていますが、NotEscapableは
// Copyableなので、コンパイラは
// 以下の使用法を満たすためにコピーを挿入できます
borrowingFunc(value) // OK
}
ノンエスケープ可能なパラメータの制約
パラメータとして受け取ったノンエスケープ可能な型の値は、他のローカル変数と同じ制約の対象となります。
特に、ノンエスケープ可能な consuming
パラメータ(およびそのすべての直接コピー)は、関数の実行中に実際に破棄されなければなりません。
これは、返却やインスタンスプロパティまたはグローバル変数への保存によって破棄できるエスケープ可能な consuming
パラメータとは対照的です。
不可エスケープ値を含む型は、不可エスケープ型でなければなりません。
構造体プロパティと列挙型ペイロードは、それらを囲む型自体が不可エスケープ型であれば、不可エスケープ型を持つことができます。
同様に、エスケープ可能な構造体または列挙型は、エスケープ可能な値のみを含むことができます。
不可エスケープ値は、クラスプロパティとして保存できません。クラスは常に本質的にエスケープされるからです。
// 例
struct EscapableStruct {
// 🛑 エスケープ可能な構造体は、非エスケープ可能な保存プロパティを持つことができません
var nonesc: Nonescapable
}
enum EscapableEnum {
// 🛑 エスケープ可能な列挙型は、非エスケープ可能なペイロードを持つことができません
case nonesc(Nonescapable)
}
struct NonescapableStruct: ~Escapable {
var nonesc: Nonescapable // OK
}
enum NonescapableEnum: ~Escapable {
case nonesc(Nonescapable) // OK
}
<a name=「Returns」></a>返されるnonescapable値にはライフタイム依存が必要
前述の通り、単純にnonescapable値を返すことは許可されていません。
func f() -> NotEscapable { // 🛑 nonescapable型を返すことはできません
var value: NotEscapable
return value // 🛑 nonescapable型を返すことはできません
}
将来の提案では、返される値の寿命を別のバインディングの寿命に結びつけることで、この要件を緩和できる「寿命依存注釈」が説明される予定です。
特に、構造体および列挙型の初期化子(新しい値を構築し、呼び出し元に返す)は、そのような仕組みがないと記述できません。
グローバル変数および静的変数には非エスケープ可能値を指定できない
非エスケープ可能な値は、特定のローカル実行コンテキストに制限する必要があります。
つまり、グローバル変数や静的変数に格納することはできないということです。
クロージャと非エスケープ可能な値
エスケープクロージャでは、非エスケープ可能な値を捕捉できません。
非エスケープクロージャでは、通常の排他制約に従う限り、非エスケープ可能な値を捕捉できます。
クロージャから非エスケープ可能値を返すことは、明示的なライフタイム依存アノテーションを使用する場合のみ可能であり、これは今後の提案で取り上げられる予定です。
非エスケープ可能値と並列処理
非エスケープ可能値を関数のパラメータや戻り値として使用する際のすべての要件は、async let
経由で呼び出されるものも含め、非同期関数にも適用されます。
Task.init、
Task.detached、または
TaskGroup.addTask` で使用されるクロージャはエスケープクロージャであるため、エスケープ不可能な値を捕捉することはできません。
Escapable
型の
条件付きで 型は、ジェネリック引数に応じてエスケープ可能になるかどうかを変更できます。
他の条件付きの動作と同様に、これは拡張機能を使用して型に新しい機能を条件付きで追加することで表現されます。
// 例:条件付きエスケープ可能なジェネリック型
// デフォルトでは、Boxはそれ自身エスケープ不可
struct Box<T: ~Escapable>: ~Escapable {
var t: T
}
// Boxは、ジェネリック引数がエスケープ可能である場合、エスケープ可能になります
//
extension Box: Escapable where T: Escapable { }
これは、他の抑制可能なプロトコルと組み合わせて使用することができます。
例えば、多くの一般的なライブラリコンテナ型は、その内容に応じてコピー可能および/または抑制可能である必要があります。
このような型を宣言する簡潔な方法は以下のとおりです。
struct Wrapper<T: ~Copyable & ~Escapable>: ~Copyable, ~Escapable { ... }
extension Wrapper: Copyable where T: Copyable, T: ~Escapable {}
extension Wrapper: Escapable where T: Escapable, T: ~Copyable {}
ソース互換性
コンパイラは、明示的な ~Escapable
指定のない型をすべてエスケープ可能として扱います。
これは現在の言語の動作と一致しています。
新しい型に ~Escapable
指定がされた場合のみ、何らかの影響が生じます。
既存の具象型に ~Escapable
を追加することは、一般的にソースの互換性を損なうことになります。なぜなら、既存のソースコードがこの型の値をエスケープできることに依存している可能性があるからです。
既存の具象型から ~Escapable
を削除することは、一般的にソースの互換性を損なうことにはなりません。なぜなら、これは事実上、新しいプロトコル準拠を追加することと同様に、新しい機能を追加することになるからです。
ABI 互換性
上記のように、既存のコードは今回の変更の影響を受けません。
既存の型に対して ~Escapable
制約を追加または削除することは、ABI を破る変更となります。
採用への影響
mangling とインターフェイスファイルは、エスケープ不可能なことを記録するだけです。
つまり、新しいコンパイラで消費される既存のインターフェイスは、すべての型をエスケープ可能として扱います。
同様に、新しいインタフェースを読み込む古いコンパイラでも、新しいインタフェースに ~Escapable
型が含まれていなければ問題は発生しません。
これらの考慮事項により、以前にコンパイルされたコードと新たにコンパイルされたコードの間で、エスケープ可能な型を共有できることが保証されます。
エスケープ可能な型引数とエスケープ不可能な型引数の両方をサポートできるように既存の汎用型を後付けすることは、注意を払えば可能です。
今後の方向性
Span
ファミリーの型
この提案は、他の場所で議論されてきた Span
型に対するニーズに大きく影響されています。
簡単に説明すると、この型は、連続したメモリに格納された配列のようなデータの効率的な汎用的な「ビュー」を提供します。
この型の値は、データを所有するのではなく、別の場所に格納されたデータを参照するだけなので、その寿命は所有するストレージの寿命を超えないように制限しなければなりません。
この型に対するサンプル実装と提案を、間もなく公開できる見込みです。
初期化子とライフタイムの依存関係
非可避関数パラメータは、関数のスコープを越えて存続することはできません。
したがって、非可避値は決して関数から返されることはありません。
非可避値は初期化子の本体で生成されます。
当然ながら、初期化子はその値を返さなければならず、これにより例外が発生します。
初期化子のパラメータは通常、nonescapable値が存続できない期間を示します。
初期化子は、例えば、独自の存続期間を持つオブジェクトにバインドされたコンテナ変数に依存するnonescapable値を作成することがあります。
struct Iterator: ~Escapable {
init(container: borrowing Container) { ... }
}
let container = ...
let iterator = Iterator(container)
consume container // `container` の寿命はここで終了します
use(iterator) // 🛑 'iterator' は `container` より長生きします
関数パラメータから、変更不可の結果への依存関係を指定するには、現在、実験的な寿命依存機能が必要です。
寿命依存機能により、変更不可型の初期化は安全になります。前述のような誤用はコンパイルエラーとなります。
寿命依存機能のための新しい構文を採用するには、別途、焦点を絞ったレビューが必要です。
それまでは、変更不可値の初期化は実験的なままです。
標準ライブラリ型の拡張
多くの標準ライブラリ型が、Optional
、Array
、Set
、Dictionary
、およびUnsafe*Pointer
ファミリー型を含む、おそらくは非回避型型をサポートするために更新される必要があることが予想されます。
これらの型の中には、まず最初に Collection
、Iterator
、Sequence
、および関連プロトコルがこれらの概念を直接採用できるかどうか、あるいは既存のものを補完するために新しいプロトコルを導入する必要があるかどうかを調査する必要があるものもあります。
Equatable、
Comparable、および
Hashable` などのより基本的なプロトコルは、更新が容易であるはずです。
with*
クロージャ取得用 API の改良
~Escapable型は、クロージャが自身の寿命を超えて引数を保存したり保持したりできないことを保証することで、一般的な
with*` クロージャ取得用 API を改良するために使用できます。
例えば、クロージャの完了時にリソースのロックを解除することを期待するロック用 API の安全性を大幅に向上させることができます。
ノンエスケープ可能なクラス
クラス型をノンエスケープ可能から明示的に除外しました。
将来的には、クラスオブジェクトに対する参照カウント操作のほとんどを回避する方法として、クラス型をノンエスケープ可能として宣言できるようにする可能性があります。
並列処理
構造化された並列処理には、この提案で概説されているものと同様の寿命制約が含まれます。
構造化された並列処理のプリミティブに ~Escapable
を組み込むのが適切かもしれません。
例えば、現在の TaskGroup
型はローカルコンテキストからエスケープされることはないはずですが、
これを ~Escapable
にすると、このような悪用を防止でき、他の最適化も可能になるかもしれません。
グローバルなエスケープ不可能型で不滅の寿命
この提案では現在、エスケープ不可能型を持つ値をグローバル変数や静的変数に入れることを禁止しています。
将来的には、「静的」または「不滅」の寿命を明示的にアノテーションすることで、これを許可できるようになることを期待しています。
検討された代替案
~Escapable
を使用せずにエスケープ可能な型を示すために Escapable
を要求する
すべてのエスケープ可能な型に Escapable
を要求することで、Escapable
プロパティを持たない型に ~Escapable
を使用せずにマークすることができます。
しかし、既存のすべての Swift コードにあるすべての既存の型に、新しい明示的な機能を使用するように更新を要求することは現実的ではありません。
それとは別に、私たちは、ほとんどの型が今後もエスケープ可能であり続けると予想しています。そのため、否定マーカーは全体的な負担を軽減します。
また、段階的な開示とも一致しています。
ほとんどのプログラミング言語のほとんどのデータ型で共通する動作であるため、Swiftを新たに学ぶプログラマーのほとんどは、エスケープ可能な型の動作の詳細を知る必要はありません。
開発者が既存のエスケープ不可能な型を使用する場合、その背後にある概念の詳細な理解がなくても、特定のコンパイラエラーメッセージが正しい使用方法をガイドするはずです。
今回の提案では、これらの概念の詳細な理解が必要となる開発者は、エスケープ不可能な型を公開したいライブラリ作成者だけです。
Nonescapable
をマーカープロトコルとして
この型では、コンパイラによる追加のチェックが必要であることを示すマーカープロトコルとして Nonescapable
を導入することを検討しました。
このアプローチでは、上記の Box
のような条件付きエスケープ可能な型を次のように定義します。
// Box は通常、追加のエスケープ可能性チェックは必要ありません
struct Box<T> {
var t: T
}
// しかし、T が追加のチェックを必要とする場合は、Box も同様です
extension Box: Nonescapable where T: Nonescapable { }
しかし、これは Nonescapable
型が
Anyのサブタイプであり、したがって
Any 存在論的ボックス内に配置できることを意味します。 Any
存在論的ボックスは Copyable
かつ Escapable
であるため、
Nonescapable な値を含むことは許可できません。
~Copyable
に依存する
Span の設計の一部として、新しい型概念を導入する代わりに ~Copyable
を使用すれば十分かどうかを検討しました。
Andrew Trick 氏の Language Support for Bufferview における分析では、Span
をコピー不可にしても、その型に求める完全なセマンティクスを提供するには不十分であるという結論に達しました。
さらに、Span
を ~Copyable
として導入すると、後に ~Escapable
に拡張することが実際上できなくなります。
この文書の冒頭のイテレータの例は、別の動機付けを提供しています。
イテレータは、コレクション内の特定のポイントを記録するために日常的にコピーされます。
したがって、この種の型に対しては、コピー不可というライフタイムの制限は適切ではないと結論付けました。そして、言語に新しいライフタイムの概念を導入する価値があると考えました。
謝辞
この提案について多くの人々が議論し、重要なフィードバックを提供してくれました。Kavon Farvardin、Meghana Gupta、John McCall、Slava Pestov、Joe Groff、Guillaume Lessard、Franz Buschなどです。
要するに、 Array をより効率化した Span という仕組み(ここではほぼ型として話している)を取り入れたいが、 Span は Array より制約を厳しくしないと安全ではない。
制約は例えば、(ここはあっているかあまりよくわかってないが) Span はほぼ確実にメモリ上ではスタックにいなければならない。
しかし現在の Swift ではその厳しい制約を表現できないので、 ~Escapable
というエスケープ不可(つまりどこからもキャプチャできない)な制約を導入することで、 Span を表現できるようにしよう。
的なやつ(たぶん)