Closed10

SE-0335: Introduce existential any

UeeekUeeek

Introduction

SwiftでExistentialType(存在型)は、型宣言のところでプロトコルの名前を使うだけで、とても簡単に表現できる。

protocol Animal {}
var dog: Animal <- 存在型

長年にわたって、プログラマに間違った使い方をさせ、変更の時に書き直しの手間を発生させるなど、これは紛らわしさの問題を発生させていた。
このプロポーザルでは、存在型を使うときに明示的にanyをつけるようにして、存在型の影響を明示的にする。

UeeekUeeek

Motivation

Swiftで存在型では、制約の大きさとパフォーマンスの問題がある。
いくつかの制限は、言語の機能が欠けていることからきているが、
多くは、型消去乗せマンティクスの基本的なことである(?)
例えば、associated Typeが必要なprotocolを考える。
存在型は、それ自身ではプロトコルを満たすことはできず、満たすように実装することが必要である。
なぜなら、そのプロトコルを満たす任意の方に対して機能する 明確な具体的なassociatedTypeがないからである。

protocol P {
  associatedtype A
  func test(a: A)
}
func generic<ConcreteP: P>(p: ConcreteP, value: ConcreteP.A) {
  p.test(a: value)
}
func useExistential(p: P) {
  generic(p: p, value: ???) // what type of value would P.A be??
}

また、存在型は具体的な方を使う場合に比べて、expensive。(メモリを多く使用する)?
なぜなら、存在型はそのプロトコルを満たす任意の型を入れることができるからである。そして、その型は動的に変えることができる。
そのため、どんなにその型のメモリ使用量が少なくても、メモリを動的に確保できるようにする必要がある。
さらに、ヒープ領域やARCのために、存在型を使うコードは、ポインタの間接参照と メソッドの動的ディスパッチをする必要がある。それらは最適化できない。 (<- https://youtu.be/HygLwTRO-Zw?si=32BjI8-ajejzi7Fj 多分この辺の話)

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

var anyAnimal: anyAnimal = Dog()
anyAnimal = Cat ()

これらの 影響が大きく、よく発生する望まない 非明示的な動作に関わらず、存在型は簡単に使用できる。
書くときは、それらの代償を意識することはなく、generics制約のような書き方は、多くのプログラマーにとって存在型とGenericsが紛らわしいものとなっている。

現実では、そのような動的な型はgenericsに比べて、あまり需要がないが、Swiftの文法的に、存在型が簡単に使えてしまうので、多くの人が誤用してしまっている。
存在型を使う代償は明示的であるべきで、使うときは 使うことを意識して使えるようにするべきである。

UeeekUeeek

Proposed solution

any keywordを使って、文法的に、明示的に、存在型を使うようにする。
Swift5のコンパイラーで利用可能。

// Swift 5 mode
protocol P {}
protocol Q {}
struct S: P, Q {}
let p1: P = S() // 'P' in this context is an existential type
let p2: any P = S() // 'any P' is an explicit existential type

let pq1: P & Q = S() // 'P & Q' in this context is an existential type
let pq2: any P & Q = S() // 'any P & Q' is an explicit existential type

In Swift 6, existential types are required be explicitly spelled with any:
// Swift 6 mode
protocol P {}
protocol Q {}
struct S: P, Q {}
let p1: P = S() // error
let p2: any P = S() // okay

let pq1: P & Q = S() // error
let pq2: any P & Q = S() // okay

Swift6の挙動は、ExistentialAny flagで有効にすることができる。

UeeekUeeek

Detailed design

明示的に存在型を宣言する文法

type -> existential-type
existential-type -> 'any' type

UeeekUeeek

存在型のセマンティクス

any型は存在型と同じ意味である。
anyは、プロトコルと、その複合体、また、メタタイプのみに適応できます。
nominal型、structured型(<- nominalとの対応)、型パラメータ、プロトコルメタタイプには使用できない。

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 and AnyObject

anyは、Any型やAnyObject型には不要である。

struct S {}
class C {}

let value: any Any = S() <- anyなくても動く
let values: [any Any] = [] <- anyなくても動く
let object: any AnyObject = C() <- anyなくても動く

protocol P {}
extension C: P {}

let pObject: any AnyObject & P = C() // anyがないとエラーになる。

AnyやAnyObjectにanyをつけるのは冗長である。それらは特別な型で、名前から明示的にわかるので、明示的にanyをつけなくてもいいという理解。

Metatypes

存在的 MetaType (P.Typeは any P.Typeになる)
プロトコルMeta Type(P.Protocolは、 (any P).Typeになる)
プロトコルMetaTypeの値 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

(Note): 存在型の Meta Typeは any P.Typeのように表現する。それは、MetaTypeの一般化だからである。
プロトコルのMetaTypeは(any P)自身のMetaTypeなので、(any P).Typeと書く。

このモデルでは、anyキーワードは、概念的には existential quantifier ∃と同じように振る舞う。
正式には、any P.Type ∃ T:P . T.Type を意味する。(Pを満たす具体的な型Tが存在し、T.Type)

any P.Typeと(any P).Type`の文法的な違いはとても些細なものである。
しかし、(any P).Typeは滅多に実用例がない。
それは 型パラメーター T が存在型に置き換えられる一般的なコンテキストを考えると、T.Type がシングルトン プロトコル メタタイプである理由を説明するのに役立ちます?

Metatypes for Any and AnyObject

基本的な型のように、Any.TypeとAnyObject.Typeは 存在型MetaTypeである。
AnyとAnyObjectのMetaTypeは(any Any).Tpye、(any AnyObject).Typeのようにかける。

Type aliases and associated types

プロトコルPに対するtypealiasは Generics制約としても、存在型としても使える。
anyは存在型なので、anyPのtypealiasは 存在型として使われる必要がある。
generics制約を満たすために使われることはできない。anyを使う側から書く必要もない。

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) { ... } // error

一度、anyがSwift6以下で必要になったら、プロトコル名のTypealiasは associatedTypeの制約として機能しなくなる?
any付きのtypealiasで書く必要がある。

// Swift 6 code

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
}
UeeekUeeek

互換性

anyを必須にするには、ソースコードの変更が必要になる。
変更を簡単にするために、swift5.6でanyをつけれるようにする(必須ではない)。Swift6から必須になる。
Swift5では、既存の非明示的な存在型の宣言は、有効なままである。
単純な変更なので、機械的に自動でできそう。

SE-309(Unlock existentials for all protocols)では、多くのコードで、存在型を使用できるようになった。
Swift6で無効になる存在型の使用方法を少なくするために、私は、Self およびassociatedTypeの要件を持つプロトコルに対して any を直ちに要求することを提案します。
それによって、Swift5のモードでは非一貫性が生じるが、それはすでに存在してるものなので大した問題ではないだろう(なぜなら、ある種のプルとコルは存在型として、全くとして、使用できないから)
そして、その構文の違いは、二つの利点がある

  • Swift6で無効になるコードを欠かせないようにすることで、将来 対応する必要がなくなり、時間の節約になる。
  • Swift6の準備として、プログラマーがanyに慣れることができる

Transitioning to any in Swift 6

anyが導入されると、compilerはwarningを表示しないようになり、コンパイルエラーになる。

UeeekUeeek

Alternatives considered

Rename Any and AnyObject

Any. AnyObjectを"any Value" "any Object"にする。
これはプロポーザルとの一貫性があるが、より多くの変更が必要になる。
AnyとAnyObjectは他の存在型より害悪でないため(Anyと明示されているから?)、変更のコストに対して、得られるメリットが小さそうなので、変更しないことにした。

Use Any<P> instead of any P

anyをキーワードじゃなくて、Any<P>みたいな書き方にする方法。
そうすると、someanyの対称性がなくなる。
anyとsomeが似た文法になっていることで、置き換えやすくなる。
事実として、anyはsomeで置き換えられる場合が多い。 someの方がメモリに優しいので?置き換えられるなら置き換えた方が良さそう。

最後に、Genericの文法と間違えやすくなる。

UeeekUeeek

Future Directions

Extending existential types

protocolのexntentionにanyをつけれるようにする。

extension any Equatable: Equatable { ... }

Re-purposing the plain protocol name

In other places in the language, a plain protocol name is already sugar for a type parameter conforming to the protocol. Consider a normal protocol extension:

extension Collection { ... }

このextentionは、Collection プロトコルと継承する、すべてのタイプに適応される。
以下のものと同じ働き。

extension <Self> Self where Self: Collection { ... }

anyの構文を変えることによって、上記の糖衣構文を拡張できる。
もし、存在型が明示的に宣言する必要があるなら、"protocol名をただ書くこと"は常に、プロトコルを継承することを要件とする型パラメータとしての糖衣構文を意味するようになる。

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

associatedTypeを"<>"で宣言する構文と組み合わせることで、上の宣言は、下のようにシンプルに描けるようになる。

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

This sugar eliminates a lot of noise in cases where a type parameter is only referred to once in a generic signature, and it enforces a natural model of abstraction, where programmers only need to name an entity when they need to refer to it multiple times.

このスクラップは13日前にクローズされました