Swift Digest
SE-0404 | Swift Evolution

Allow Protocols to be Nested in Non-Generic Contexts

Proposal
SE-0404
Authors
Karl Wagner
Review Manager
Holly Borla
Status
Implemented (Swift 5.10)

01 何が問題だったのか

Swift では struct / class / enum / actor といった nominal type を他の nominal type の内側にネストして宣言できます。たとえば String.UTF8Viewstruct String の内側に struct UTF8View としてネストされており、名前自体が「String の UTF-8 code units に対するインタフェース」という役割を自然に表しています。

しかし、これまでは protocol だけはネストが許されておらず、常にモジュール直下のトップレベル型として宣言しなければなりませんでした。そのため、ある型に強く紐づいた protocol であっても、その所属関係を名前空間の構造として表現できず、TableView とそのデリゲート protocol を書きたい場合には次のように TableViewDelegate のような複合名で所属関係を示すしかありませんでした。

class TableView { /* ... */ }

protocol TableViewDelegate: AnyObject {
  func tableView(_: TableView, didSelectRowAtIndex: Int)
}

同様に、関数やクロージャの内側で、その関数のためだけに使う小さな抽象を protocol として切り出したいケース(Swift コンパイラ自身を含め、大きなクロージャの中で型と抽象を組み立てるコードはしばしば登場します)でも、protocol だけはそこに書けないという不自然な制約が残っていました。

02 どのように解決されるのか

非ジェネリックな struct / class / enum / actor、および非ジェネリックな文脈に属する関数の内側に、protocol をネストして宣言できるようにします。これにより、ある型に自然に紐づく protocol をその型の内側で表現でき、複合名で所属関係を示す必要がなくなります。

型の内側にネストする

たとえば TableView のデリゲート protocol は、TableView の内側に Delegate としてネストできます。外からは TableView.Delegate という名前で参照でき、所属関係が名前空間として明示されます。

class TableView {
  protocol Delegate: AnyObject {
    func tableView(_: TableView, didSelectRowAtIndex: Int)
  }
}

class DelegateConformer: TableView.Delegate {
  func tableView(_: TableView, didSelectRowAtIndex: Int) {
    // ...
  }
}

他のネスト型と同様、外側の型の文脈の中では短い名前で参照できます。次のコードでは TableView の本体内から単に Delegate と書けます。

class TableView {
  weak var delegate: Delegate?

  protocol Delegate { /* ... */ }
}

関数やクロージャの内側にネストする

非ジェネリックな関数やクロージャの内側にも protocol をネストできます。ネストされた protocol への適合も同じ関数の内側で行う必要があるため用途は限られますが、関数内で完結する小さな抽象を素直に表現できます。

func doSomething() {
  protocol Abstraction {
    associatedtype ResultType
    func requirement() -> ResultType
  }
  struct SomeConformance: Abstraction {
    func requirement() -> Int { /* ... */ }
  }
  struct AnotherConformance: Abstraction {
    func requirement() -> String { /* ... */ }
  }

  func impl<T: Abstraction>(_ input: T) -> T.ResultType {
    // ...
  }

  let _: Int = impl(SomeConformance())
  let _: String = impl(AnotherConformance())
}

ジェネリックな文脈ではネストできない

protocol をネストできるのは 非ジェネリックな文脈に限られます。ジェネリック型・ジェネリック関数、および「ジェネリック型のメンバ関数」のような間接的にジェネリックな文脈も含めて、いずれの内側にも protocol はネストできません。

class TableView<Element> {
  protocol Delegate {  // error: protocol 'Delegate' cannot be nested within a generic context.
    func didSelect(_: Element)
  }
}

func genericFunc<T>(_: T) {
  protocol Abstraction {  // error: protocol 'Abstraction' cannot be nested within a generic context.
  }
}

class TableView<Element> {
  func doSomething() {
    protocol MyProtocol {  // error: cannot be nested within a generic context.
    }
  }
}

これを許すには、ジェネリック protocol を導入するか、外側のジェネリックパラメータを associated type にマッピングするといった設計が必要になるため、この提案のスコープ外になっています。

associated type の witness にはならない

具象型の内側にネストされた protocol は、同名の associated type 要件の witness にはなりません。associated type は「ひとつの具象型にひとつの適合型を対応づける」ものですが、protocol は多くの具象型が適合できる制約型であり、ここで対応づけるべきものが一意に定まらないためです。

protocol Widget {
  associatedtype Delegate
}

struct TableWidget: Widget {
  // Widget.Delegate の witness にはならない。
  protocol Delegate { /* ... */ }
}

採用と移行に関する注意

この機能は単なる拡張なので自由に採用・撤回できますが、ある protocol をトップレベルからネストに(あるいはその逆に)移動すると、ソース互換性・ABI 互換性のいずれも壊れます。ソース側の breakage は、旧名から新名への typealias を用意することで緩和できます。ABI 側は、外側の文脈が mangled name の一部になるため、移動は ABI 非互換な変更になります。

今後の見通し

この提案の延長として、次のような方向性が議論の対象として挙げられています(speculative で、実現が約束されているわけではありません)。

  • protocol の内側に、非 protocol の型(struct / class / enum 等)をネストできるようにする。たとえば標準ライブラリの FloatingPointRoundingRule のような「その protocol のためだけに存在する型」を、自然に protocol の内側に書けるようになります。
  • ジェネリック型の内側にも protocol をネストできるようにする。ジェネリック protocol や、外側のジェネリックパラメータを associated type にマッピングする仕組みなどが検討対象です。