Swift Digest
SE-0427 | Swift Evolution

noncopyable generics

Noncopyable Generics

Proposal
SE-0427
Authors
Kavon Farvardin, Tim Kientzle, Slava Pestov
Review Manager
Holly Borla, Ben Cohen
Status
Implemented (Swift 6.0)

このダイジェストはClaude Opus 4.7 / 4.8によって生成されたものです(License)。原文はこちら

01 何が問題だったのか

SE-0390 によってnon-copyableな struct / enum を宣言できるようになりましたが、既存のジェネリクス・プロトコル・existentialはすべて「値はコピーできる」という前提の上に組み立てられていたため、non-copyableな型はそれらとほぼ連携できませんでした。

具体的には次のような制約がありました。

  • non-copyableな型をほかのジェネリック型の型引数にできない。たとえば FileDescriptor: ~CopyableOptional<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 への適合が暗黙に入ります。

  1. struct / enum / class 宣言
  2. ジェネリック型パラメータ宣言
  3. プロトコル宣言
  4. associated type宣言(今回の提案では抑制不可)
  5. protocol extensionの Self
  6. 具体型に対する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: CopyableT は型のジェネリックパラメータ)の形でなければならず、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なジェネリックパラメータを持てます。

  1. パラメータを値として直接保持しない場合。() -> TT がnon-copyableでも関数自体はコピー可能です。

    struct Factory<T: ~Copyable> /* : Copyable */ {
      let fn: () -> T  // ok
    }
    
  2. 型がクラスの場合。クラスのプロパティはコピーされないため、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

03 今後の見通し

今回の提案ではスコープ外とされた、次のような発展の構想が示されています。いずれも将来の方向性として述べられているもので、実現を約束するものではありません。

associated typeでの Copyable 抑制

associated typeにも ~Copyable を書いて Copyable 要件を抑制できるようにする、より一般的な仕組みです。今回の提案でも一度は実装が試みられましたが、既存プロトコルに後から ~Copyable を導入した場合のソース互換性の問題が大きく、本格的な対応は別の提案に持ち越されました。たとえば IteratorProtocolElement をnon-copyableにできれば、mutating func next() -> Element? がnon-copyableな要素を返せるようになりますが、これを既存コードを壊さずに実現する設計には未解決の課題が残っています。

標準ライブラリのnon-copyable対応

OptionalUnsafePointer 系の型に対するnon-copyable対応は、比較的素直に進められる領域として挙げられています。さらに先には、non-copyable要素を扱えるコレクションなども視野に入っていますが、いずれも別途設計作業が必要とされています。

non-copyableなタプルとparameter pack

タプルやparameter packをnon-copyableに一般化する案も、別の提案として議論される予定とされています。non-copyableな値を素直にまとめて扱えるようにするための自然な拡張という位置付けです。

~Escapable への展開

「現在のコンテキストから値が脱出(escape)できる」という前提も、Swiftのすべての型が暗黙に持つ性質です。これを ~Escapable で抑制できるようにすれば、オブジェクトの寿命を別の角度から制御する手段が得られます。~Copyable と同じ枠組みの応用として、companion proposalで詳しく扱う構想です。