Noncopyable Generics
01 何が問題だったのか
SE-0390 によってnon-copyableな struct / enum を宣言できるようになりましたが、既存のジェネリクス・プロトコル・existentialはすべて「値はコピーできる」という前提の上に組み立てられていたため、non-copyableな型はそれらとほぼ連携できませんでした。
具体的には次のような制約がありました。
-
non-copyableな型をほかのジェネリック型の型引数にできない。たとえば
FileDescriptor: ~CopyableをOptional<FileDescriptor>にできないため、failable initializerすら定義できませんでした。struct FileDescriptor: ~Copyable { init?(filename: String) { // error: cannot form a Optional<FileDescriptor> // ... } } - non-copyableな型はプロトコルに適合できない(例外は
Sendableのみ)。 - non-copyableな型をexistentialにボックス化できない。
ジェネリクスやプロトコルは実用的なSwiftコードの中核であり、Optional のような標準的なラッパーすら使えない状況では、non-copyableな型が活躍できる範囲が大きく制限されてしまいます。「値はコピーできる」という暗黙の前提を、既存コードの意味を壊さずに緩める統一的な仕組みが必要でした。
02 どのように解決されるのか
コピー可能性を、標準ライブラリの新しいプロトコル Copyable として明示的に表現します。そのうえで、ジェネリック型パラメータ・プロトコル・existentialなどの各種宣言位置に暗黙の Copyable 適合を入れ、必要に応じて ~Copyable でそれを抑制(suppress) できるようにします。
~Copyable は「Copyable でないこと」を表す否定ではなく、「暗黙の Copyable 要件を書かなかったことにする」という純粋に構文糖的な操作である点が重要です。抑制された側は、copyableな型もnon-copyableな型も受け入れられる、より緩い要件を持つことになります。
progressive disclosureが設計の柱です。~Copyable を書かない限り従来通りの世界にとどまるため、non-copyableジェネリクスを知らない利用者が既存コードの意味を意識する必要はありません。
Copyable プロトコル
Copyable は値がコピーできる型を抽象化する特別なプロトコルで、明示的な要件は持ちません。メタタイプやタプルのように通常は他のプロトコルに適合できない型も Copyable には適合する、という扱いになっています。
Copyable にprotocol extensionを書くことはできません。すべてのcopyableな型にメンバーが追加されてしまい、オーバーロード解決が混乱するためです。
extension Copyable { // error
func f() {}
}
暗黙の Copyable 適合と抑制位置
次の位置では、明示的に抑制しない限り Copyable への適合が暗黙に入ります。
struct/enum/class宣言- ジェネリック型パラメータ宣言
- プロトコル宣言
- associated type宣言(今回の提案では抑制不可。Future Directions参照)
- protocol extensionの
Self - 具体型に対するextensionのジェネリックパラメータ
struct Polygon /* : Copyable */ {}
func identity<T>(x: T) /* where T: Copyable */ -> T { x }
protocol Shape /* : Copyable */ {}
これに対して ~Copyable は、「その位置の暗黙 Copyable 要件を書かなかったことにする」操作として解釈されます。たとえばnon-copyableな型も受け取れる恒等関数は次のように書けます。
func identity<T: ~Copyable>(x: consuming T) -> T { x }
non-copyableなジェネリックパラメータを関数のパラメータ型として使う場合は、concreteなnon-copyable型と同じく borrowing / consuming / inout のいずれかの所有権修飾子(SE-0377)を付ける必要があります。
プロトコル側で Copyable を抑制すれば、non-copyableな型もそのプロトコルに適合できるようになります。
protocol Resource: ~Copyable {
consuming func dispose()
}
extension FileDescriptor: Resource { /* ... */ }
copyableな型は、このような ~Copyable プロトコルに問題なく適合できます。
抑制が効かない場合
他の要件によって Copyable が強制されるなら、~Copyable を書いても抑制できません。たとえば Shape がデフォルトで Copyable を含むなら、次はエラーです。
protocol Shape /* : Copyable */ {}
func f<T: Shape & ~Copyable>(_: T) {} // error: T: Copyable は Shape から含意される
また、抑制できるのはいちばん内側のスコープで宣言されたジェネリックパラメータに限られます。外側の型が Copyable を要求している以上、内側のメソッドだけそこを緩めても整合しないためです。
struct S<T /* : Copyable */> {
func f<U /* : Copyable */>(_: T, _: U) where T: ~Copyable {} // error
}
protocol compositionのメンバー位置にも ~Copyable を書けるため、次の3つは同じ意味になります。
func f<T: Resource & ~Copyable>(_: T) {}
func f<T>(_: T) where T: Resource & ~Copyable {}
func f<T>(_: T) where T: Resource, T: ~Copyable {}
extensionの扱い
~Copyable は要件の抑制にすぎないため、extensionでは改めてデフォルトの Copyable 要件が入ります。これは、既存型にあとから ~Copyable を足しても、それ以前に書かれたextensionの意味が勝手に変わらないようにするための設計です。
struct Pair<T: ~Copyable>: ~Copyable { /* ... */ }
extension Pair /* where T: Copyable */ { /* ... */ } // T は Copyable
extension Pair where T: ~Copyable { /* ... */ } // T に制約なし
ネストされた型のextensionでも、外側・内側のそれぞれのジェネリックパラメータに対して個別に抑制できます。
struct Outer<T: ~Copyable> {
struct Inner<U: ~Copyable> {}
}
extension Outer.Inner /* where T: Copyable, U: Copyable */ {}
extension Outer.Inner where T: ~Copyable /* , U: Copyable */ {}
extension Outer.Inner where /* T: Copyable, */ U: ~Copyable {}
一方、もともとジェネリックパラメータが Copyable を要求している型(たとえば ~Copyable を付けていない型)のextensionで、改めて ~Copyable を書くことはできません。
struct Horse<Hay> {}
extension Horse where Hay: ~Copyable {} // error
protocol extensionも同様に、既存コードの意味を保つため Self: Copyable が暗黙に入ります。non-copyableなconforming typeも扱いたい場合は、extension側でも ~Copyable を明示します。
protocol EventLog: ~Copyable { /* ... */ }
extension EventLog /* where Self: Copyable */ {
func duplicate() -> Self { copy self } // OK: Self は Copyable 扱い
}
extension EventLog where Self: ~Copyable {
// non-copyable な conforming type も含めた汎用的な extension
}
プロトコル継承
~Copyable は暗黙要件の抑制でしかなく、継承によって伝搬しません。派生プロトコル側でも Copyable を抑制したければ改めて書く必要があります。
protocol Token: ~Copyable {}
protocol ArcadeToken: Token /* , Copyable */ {} // Self: Copyable が復活
protocol CasinoToken: Token, ~Copyable {} // 引き続き non-copyable 可
条件付きの Copyable 適合
struct / enum は、ジェネリックパラメータがcopyableなときだけ自身もcopyableになる、という条件付き適合を書けます。
enum List<T: ~Copyable>: ~Copyable {
case empty
indirect case element(T, List<T>)
}
extension List: Copyable where T: Copyable {}
これで List<Int> はcopyable、List<FileDescriptor> はnon-copyableという扱いになります。他のプロトコルへのextensionとは違って、Copyable 適合を宣言するextensionは追加の要件を自動で含みません。必要なら where 節を明示的に書く必要があります。
条件付き Copyable 適合には、次の制約があります。
- 条件節は
T: Copyable(Tは型のジェネリックパラメータ)の形でなければならず、T == Array<Int>のような同値要件や、associated typeへの要件は使えません。 - 型が
deinitを持つ場合、条件付き適合はできません。deinitによる決定的な破棄を保証するため、non-copyableであることを無条件に確定させる必要があります。 - 条件付き
Copyable適合は、対応する型と同じソースファイルに書かなければなりません。コピー可能性は型に深く根ざした性質だからです。
Copyable への適合は、struct ならすべての stored property、enum なら各ケースの関連値が Copyable であることをコンパイラがチェックします。そのため、non-copyableな値を直接フィールドに持ちながら無条件に Copyable を主張する型は書けません。
struct Holder<T: ~Copyable> /* : Copyable */ {
var value: T // error
}
ただし次の2つのケースでは、copyableな型がnon-copyableなジェネリックパラメータを持てます。
-
パラメータを値として直接保持しない場合。
() -> TはTがnon-copyableでも関数自体はコピー可能です。struct Factory<T: ~Copyable> /* : Copyable */ { let fn: () -> T // ok } -
型がクラスの場合。クラスのプロパティはコピーされないため、non-copyableな値を
letで保持できます。class Box<T: ~Copyable> { let value: T // ok init(value: consuming T) { self.value = value } }
クラスの扱い
クラスはnon-copyableなジェネリックパラメータを持てますが、クラス自身を ~Copyable にすることはできません。また、AnyObject 要件や superclass 要件と ~Copyable を組み合わせることもできません。
func f<T>(_ t: T) where T: AnyObject, T: ~Copyable {} // error
existential
existentialの制約部分はprotocol compositionとして解釈され、そこにも暗黙の Copyable メンバーが入ります。したがって Any は実際には any Copyable を指し、すべての型の上位型は any ~Copyable です。
any ~Copyable
/ \
/ \
Any == any Copyable <非 Copyable な全型>
|
<Copyable な全型>
~Copyable をcomposition memberとして書けば、existentialもnon-copyable型を受け入れるようになります。
protocol Pizza: ~Copyable {}
struct UniquePizza: Pizza, ~Copyable {}
let t: any Pizza /* & Copyable */ = UniquePizza() // error
let _: any Pizza & ~Copyable = UniquePizza() // ok
Future Directions
今回のスコープ外として、次のような発展が示されています(speculativeなもので、実現を約束するものではありません)。
- associated typeの
Copyableを抑制できるようにする仕組み。今回の提案ではソース互換性の問題が大きく先送りされました。 OptionalやUnsafePointer系をはじめとする標準ライブラリのnon-copyable対応。- non-copyableなタプルやparameter packへの一般化。
- 「エスケープできる」という暗黙の前提を抑制する
~Escapableによるライフタイム制御。