Skip to content

Latest commit

 

History

History
382 lines (257 loc) · 14.4 KB

Swift 存在型(Existential type)とanyキーワード.md

File metadata and controls

382 lines (257 loc) · 14.4 KB

Swift 存在型(Existential type)とanyキーワード

収録日: 2022/01/16

概要

Swift6で存在型(Existential type)を書く際にanyキーワードが必須になる。その理由や内容について見ていく。

内容

存在型(Existential type)とは?

ある条件を満たす全ての型を表す型。Swiftではプロトコルを型として使用した場合にがこれに該当する。

例: Animalプロトコルに準拠したDog構造体とCat構造体はAnimal型として扱うことができる。

protocol Animal {}
struct Dog: Animal {}
struct Cat: Animal {}

let animals: [Animal] = [Dog(), Cat()]

存在型には、異なる具体的な型を代入できる。

var dog: Animal = Dog()
dog = Cat() // ok

関連ドキュメント: https://docs.swift.org/swift-book/LanguageGuide/Protocols.html#ID275

存在型の制限や問題

存在型を使用する際、下記のような機能面の制限やパフォーマンス面の問題があることを認識する。

機能面での制限

  • 型として利用できない場合がある。

下記の場合、プロトコルを型として扱うことはできない。

  1. 関連型(associated type)が要件に含まれている
  2. メソッド/プロパティ/subscript/イニシャライザの共変ではない位置(引数など)にSelfへの参照が含まれている

※ ただし、この問題は解決される予定。

より詳細はSwift PAT問題を解消して全てのプロトコルを存在型として使えるようにに記載。

関連ドキュメント: https://github.com/apple/swift-evolution/blob/main/proposals/0309-unlock-existential-types-for-all-protocols.md

  • 型情報が消去されているため、使える機能が制限される。

例えば、下記の2つのcalleeの戻り値の型は同じであることがわからないので加算できない。

func callee() -> Numeric {
    if Bool.random() {
        return 42
    } else {
        return 42.0
    }
}

func caller() {
    let x = callee() + callee() // ❌ Binary operator '+' cannot be applied to two 'Numeric' operands
}
  • 存在型はそのプロトコル自身に準拠できない。これはプロトコル自身がそのプロトコルの全ての要件を満たすことができない場合があるため(staticプロパティやイニシャライザなど)。全てを満たすことができる場合もあるが、例えば外部から提供されるプロトコルにプロトコル自身が満たせない要件が追加された場合にsource breakが発生してしまうため、これを防ぐ役割もある。
protocol P {
    func test()
}

func generic<ConcreteP: P>(p: ConcreteP) {
    p.test()
}

func useExistential(p: any P) {
    generic(p: p) // ❌ Protocol 'P' as a type cannot conform to the protocol itself
}

関連ドキュメント: https://github.com/apple/swift/blob/main/userdocs/diagnostics/protocol-type-non-conformance.md

パフォーマンス面の問題

  • プロトコルは具体的な型を動的に保持するため、動的なメモリが必要になる。たとえ3単語バッファのようなスタックに収まる小さいものでもヒープ領域と参照カウンタを使う。

下記を見てみると、プロトコルは動的な型の情報を保持するためにあらかじめメモリを確保しておく必要がある。(classは実際のオブジェクトのメモリアドレスを保持するために必要なメモリが確保される)

protocol Animal {}
struct Dog: Animal {}
class Cat: Animal {}

let animal: Animal = Dog()
let dog = Dog()
let cat = Cat()

MemoryLayout.size(ofValue: animal) // 40
MemoryLayout.size(ofValue: dog) // 0
MemoryLayout.size(ofValue: cat) // 8

関連ドキュメント: https://devstreaming-cdn.apple.com/videos/wwdc/2015/414sklk5h2k3ki3/414/414_building_better_apps_with_value_types_in_swift.pdf

  • メソッドのダイナミックディスパッチやポインタの間接参照があるため、コンパイラによる最適化ができない。

下記の例で見てみると、変数animalの値は実行時までわからないため、コンパイル時に型を決められない。

protocol Animal {
    func bark()
}
struct Dog: Animal {
    func bark() { print("bowwow") }
}

class Cat: Animal {
    func bark() { print("meow") }
}

var animal: Animal = Dog()
animal.bark() // bowwow

animal = Cat()
animal.bark() // meow

ジェネリック制約としてのプロトコルと存在型としてのプロトコル

Swiftでは、ジェネリックの制約にも存在型にもプロトコルを使用し、スペリングも似ている。そのため、ジェネリックを扱う際に両方を混同しがちになる。 さらに、存在型はジェネリックの制約を設定するよりもはるかに簡単に記述できることから、上記のようなコストがあることを意識せずに存在型を使ってしまっている場合がある。

関連ドキュメント:

解決案

そこで、存在型が使用されている場面では、anyキーワードを付けることを必須にすることで、存在型の誤用を防ぎたい。 エラーにはならないが、Swift5.6から導入予定。

※ Swift5とSwift6以降で挙動が異なる。

  • Swift5 モード
protocol P {}
protocol Q {}
struct S: P, Q {}

let p1: P = S() // ⭕️ この P は存在型
let p2: any P = S() // ⭕️ any P は明示的に存在型

let pq1: P & Q = S() // ⭕️ この P & Q は存在型
let pq2: any P & Q = S() // ⭕️ any P & Q は明示的に存在型
  • Swift6 モード
protocol P {}
protocol Q {}
struct S: P, Q {}

let p1: P = S() // ❌ エラー
let p2: any P = S() // ⭕️

let pq1: P & Q = S() // ❌ エラー
let pq2: any P & Q = S() // ⭕️

anyの使い方

プロトコル、プロトコル合成、それらのメタタイプに使える

逆にstructenumtupleなどの名前的型(nominal type)、tupleやOptional、関数型などの構造的型(structural type)、関数、ジェネリックの型パラメータなどには使えない。

struct S {}
 
let s: any S = S() // ❌ error: 'any' has no effect on concrete type 'S'
 
func generic<T>(t: T) {
  let x: any T = t // ❌ error: 'any' has no effect on type parameter 'T'
}
 
let f: any ((Int) -> Void) = generic // ❌ error: 'any' has no effect on concrete type '(Int) -> Void'

プロトコル合成以外のAnyとAnyObjectにはanyが不要

AnyAnyObjectはすでに存在型だと明白なので2度手間になる。ただし、付けてもワーニングなどは出ない。(Accepted時に変更された)

struct S {}
class C {}
 
let value: any Any = S()
let values: [any Any] = []
let object: any AnyObject = C()
 
protocol P {}
extension C: P {}
 
let pObject: any AnyObject & P = C() // ⭕️

メタタイプ

存在型のメタタイプにはanyを付ける。例えば、Pに準拠した型自体のメタタイプ(P.Type)はany P.Typeになる。Pプロコトルのメタタイプ(P.Protocol)は、any Pをかっこで囲んで(any P).Typeになる。Pプロコトルのメタタイプの値(P.self)はany Pをかっこで囲んで.selfを付ける。(any P).self

存在型のメタタイプは複数存在するが、プロトコルのメタタイプは一つしか存在しない。

protocol P {}
struct S: P {}
 
let existentialMetatype: any P.Type = S.self
 
protocol Q {}
extension S: Q {}
 
let compositionMetatype: any (P & Q).Type = S.self
 
let protocolMetatype: (any P).Type = (any P).self

タイプエイリアスとassociated type

プレーンなプロトコルの名前と同様に、プロトコルのタイプエイリアスもジェネリック制約と存在型の両方で使える。anyが付いていると明らかに存在型なので、any Pのタイプエイリアスはジェネリック制約としては使えず、存在型のみに利用できる。

protocol P {}
typealias AnotherP = P
typealias AnyP = any P
 
struct S: P {}
 
let p2: any AnotherP = S()
let p1: AnyP = S()
 
func generic<T: AnotherP>(value: T) { ... }
func generic<T: AnyP>(value: T) { ... } // ❌

Swift6になると、プレーンなプロトコルのタイプエイリアスは関連型として妥当ではなくなり、タイプエイリアスの中でanyを指定する必要がある。

  • Swift6 モード
protocol P {}
 
protocol Requirements {
  associatedtype A
}
 
struct S1: Requirements {
  typealias A = P // ❌ error: associated type requirement cannot be satisfied with a protocol
}
 
struct S2: Requirements {
  typealias A = any P // ⭕️ okay
}

Swift6への移行

方法はまだ未定だが、migratorで自動で変換されるという記載はある。

段階的な導入として

  • Swift5.6から利用可能だが、必要な箇所でanyがなくてもワーニングは出ない
  • 1つ以上先のメジャーリリース※でワーニングを導入して新しい構文の導入を促す
  • 最終的に新しいメジャーな言語バージョン(Swift6を想定)では、古い構文(anyなしの存在型)はエラーになる

※ 5.6や5.7などをメジャーリリースと呼ぶ(5.5.1などをポイントリリースと呼ぶ)

Swift5.6から導入する理由

Unlock existential for all protocols によってExistentialをより多くのコードで使えるようになった。そのためSwift6で不正になるコードを減らすためにもSwift5.6から導入するのが良いと判断。

将来的な話

Existential typeの拡張

Existentialをそのプロトコル自身に適合できるようにする。こうすることで、型消去の型をわざわざ作成しなくてもextensionで対応できるようになる場合がある。

extension any Equatable: Equatable { ... }

プレーンなプロトコル名の転用

存在型のシンタックスを変えることで、プレーンなプロトコル名の利用方法を変えることができるかもしれない。

例えば、プレーンなプロトコル名は、プロトコルの要件を満たす囲まれたコンテキスト(extensionなど)内で、常にジェネリックの型パラメータのシンタックスシュガーとみなすことができる。

extension Collection { ... }

// 上記と同じと見なせる
extension <Self> Self where Self: Collection { ... }

これによって今提案されている他の機能と組み合わせることで、よりコードを簡潔に書けるかもしれない。

例えば、現在のArrayのappend(contentsOf:)

extension Array {
  mutating func append<S: Sequence>(contentsOf newElements: S) where S.Element == Element
}

という形だが、associated typeをカッコ内に書けるようになると、もっと簡単に定義できる。

extension Array {
  mutating func append(contentsOf newElements: Sequence<Element>)
}

参考リンク

Forums

プロポーザルドキュメント

関連PR

その他