😬

[Swift6に向けて] SE-0335: Introduce existential any

2024/03/28に公開

要約

変更点

存在型の使用するときにanyキーワードが必須になる。

protocol Animal {}

//ASIS:
var animal: Animal <- OK

//TOBE:
var animal: Animal <- NG
var animal: any Animal <- OK

いつから

Swift6で導入される
Feature Flagで有効にできる(ExistentialAny)

対応の難易度

小(機械的な変更なので、ツールなどを使えるなら)

影響がありそうなこと

存在型をanyをつけずに使用している箇所がコンパイルエラーになる。

対応すべきこと

FeatureFlagを有効にしてor Swift6になってから、コンパイルエラーを直す。
ツールやXcodeの"fix issue"で自動で修正できそう。(?)

Proposalの内容

背景

Swiftでは、プロトコルの名前を型として使うことで、存在型(existential type)をとても簡単に書くことができる。

protocol Animal {}

var animal: Animal // <- 存在型の変数

存在型を簡単に書けることによって、プログラマーは間違った使い方をしやすく、 混乱を引き起こしてきた。

モチベーション

Swiftの存在型には、大きな制約とパフォーマンスへの影響の問題がある。

制約

型が抽象的になることで、コードが書きにくくなる場合がある。
存在型はそれ自身は、プロトコルを準拠するわけではないのでassociatedtypeと組み合わせて使うときに、以下のような問題に遭遇することがある。

protocol P {
  associatedtype A
  func test(a: A)
}

///Genericな関数 (具体的なPの型)
func generic<ConcreteP: P>(p: ConcreteP, value: ConcreteP.A) {
  p.test(a: value)
}

///以下の関数を呼ぶときに、P.Aの型を表現できない。
func useExistential(p: P) { //<- any P
  generic(p: p, value: ???) // what type of value would P.A be??
}

パフォーマンスの問題

存在型には、そのプロトコルを準拠する任意の型を代入できる(実行時に動的に変化する可能性がある)
その性質から、存在型は具体的な型に比べて、メモリを多く使用する。
ポインタの間接参照やメソッドの動的ディスパッチが必要になり、それらは最適化が難しい。

protocol Animal {}
struct Dog: Animal {
    var aInt: Int
}

struct Cat: Animal {
    var veryBigData1: VeryBigData
    var veryBigData2: VeryBigData
    ...
    var veryBigData100: VeryBigData
    }
}

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

var animalには、プロトコルを準拠した任意の型を代入できるが、プロトコルを準拠する側の実装によって必要なメモリサイズが変わる。(e.g. Dogは小さいvs Catは大きい。どちらの型にも対応しないといけない)
これを実現するために、var anyAnimalはポインタ型の変数となり、実体のメモリは別途確保する必要がある(間接参照)。

存在型は上記のような問題があるが、文法上では代償を意識することなく簡単に表現できる。
またGeneric制約と似ていて、プログラマーにとって、勘違いの元となっている。

実際には、存在型のような動的な型はGenericsに比べて必要な場面が少ないが、存在型が簡単に表現できてしまうことで、誤用されてしまっている。
存在型を使う代償はプログラマーに明示的であるべきで、使うときは、使うことを意識させるべきである。

新しい提案

存在型には、anyキーワードをつけるように強制し、存在型の使用を文法的に明示させる。
Swift5からこの文法は使用でき、Swift6ではanyを使用しないとコンパイルエラーになる。

Swift6の挙動は ExistentialAny flagで有効にできる。

// Swift 5 mode
protocol P {}
protocol Q {}
struct S: P, Q {}
let p1: P = S() // 'P'は存在型として使われている  <- Swift6ではコンパイルエラー
let p2: any P = S() // 'any P'として存在型の使用を明示する

let pq1: P & Q = S() // 'P & Q' も存在型 <- Swift6ではコンパイルエラー
let pq2: any P & Q = S() // 'any P & Q'として存在型の使用を明示する

既存のコードとの互換性

存在型の使用時にanyを必須にすることで、既存のコードは変更が必要になる。
Swift5.6からは、anyを使用でき、Swift6から必須となる。
単純な変更で、ツールやXcodeの機能(Fix issue)などを使えば機械的に自動で対応できそう。

影響がありそうなこと

存在型をanyをつけずに使用している箇所がコンパイルエラーになる。
FeatureFlagを有効にしてor Swift6になってから、コンパイルエラーを直す。
ツールやXcodeの"fix issue"で自動で修正できそう。

実際のプロジェクトで対応してみた

featureFlagを有効にすると、コンパイルエラーがでるようになった。
20個くらいエラーがでる → 治す→ 20個くらい別の箇所でエラーがでる …
これの繰り返しで、手動で対応するのは、たいへんそうだった。(途中であきらめた)
Xcodeの"Fix all issues"で対応できるという噂もあったが、私の環境ではできなかった。(Xcode15.1)
ツールか何かで自動かできれば楽そうだが、そうでないなら簡単な作業を繰り返すことになりそう。

まとめ

存在型は、いくつかの問題があるものの、文法上簡単で非明示的に表記できたため、多くの人が誤用していた。
その問題点を意識して使用させるために、 Swift6より存在型を定義するときにanyキーワードが必須になる。

protocol Animal {}

//ASIS:
var animal: Animal <- OK

//TOBE:
var animal: Animal <- NG
var animal: any Animal <- OK

存在型をanyをつけずに使用している箇所がコンパイルエラーになるので、修正が必要。

Refs

Discussion