Swift Digest
SE-0347 | Swift Evolution

Type inference from default expressions

Proposal
SE-0347
Authors
Pavel Yaskevich
Review Manager
Doug Gregor
Status
Implemented (Swift 5.7)

01 何が問題だったのか

ジェネリックな型パラメータを持つパラメータに対して、具象型のデフォルト値をそのまま割り当てることがこれまでできませんでした。たとえば、

func compute<C: Collection>(_ values: C = [0, 1, 2]) { // error
  ...
}

と書くと、「[Int] 型のデフォルト値は C 型に変換できない」というコンパイルエラーになります。これは、デフォルト値の型が、呼び出し側で推論される Cあらゆる具象型 に対して成り立っていなければならない、というルールに基づいた仕様上の制約です。

この制約を回避するには、デフォルト値付きのオーバーロードを別途用意するか、制約付き extension で型パラメータを具象型に縛る必要がありました。

struct Box<F: Flags> {
  init(dimensions: ..., flags: F) { ... }
}

// F を DefaultFlags に縛った extension でオーバーロードする
extension Box where F == DefaultFlags {
  init(dimensions: ..., flags: F = DefaultFlags()) { ... }
}

この手法には次の問題がありました。

  • 関数・subscript・enum の case のように、型パラメータが型ではなくメンバに属する場合には制約付き extension が使えません。
  • メンバごとにオーバーロードが増え、デフォルト値を持つパラメータが複数あると組み合わせ爆発が起きます。
  • enum はケースをオーバーロードしたり extension で宣言したりできないため、この方法自体が成立しません。
  • やむを得ず any Flags のような existential を使ってしまい、本来不要な動的ディスパッチや性能・意味論上のコストを抱え込む例も見られました。

要するに、「ほとんどの呼び出しでは既定のフラグでよい」というごく素直な API を表現するだけのために、制約付き extension・オーバーロード・existential といった重い道具立てが必要になっていた、という問題です。

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

デフォルト式(default expression)から型パラメータを推論できるようにします。具象型のデフォルト値が書かれており、かつ呼び出し側で引数が省略された場合には、そのデフォルト式の型を使って型パラメータを決定します。

struct Box<F: Flags> {
  init(dimensions: ..., flags: F = DefaultFlags()) {
    ...
  }
}

Box(dimensions: ...)                     // F は DefaultFlags に推論される
Box(dimensions: ..., flags: CustomFlags()) // F は CustomFlags に推論される

制約付き extension もオーバーロードも不要になり、関数・メソッドや enum の case にも同じ書き方で適用できます。

enum Box<D: Dimensions, F: Flags> {
  case flatRate(dimensions: D = [...], flags: F = DefaultFlags())
  case overnight(dimensions: D = [...], flags: F = DefaultFlags())
}

extension Box {
  func ship<F: ShippingFlags>(_ flags: F = DefaultShippingFlags()) {
    ...
  }
}

推論が許可される条件

デフォルト式からの型推論は、次の条件をすべて満たす宣言でのみ許可されます。満たさない場合はコンパイルエラーになります。

  • 推論対象の型パラメータが、該当パラメータの型そのもの(<T>(_: T = ...))か、そのネスト位置(<T>(_: [T?] = ...) など)に現れること。
  • 同じ型パラメータがパラメータリスト中の他の位置に登場しないこと。たとえば <T>(_: T, _: T = ...)<T>(_: [T]?, _: T? = ...) は不可です。暗黙の型合流による分かりにくい挙動を避けるため、型衝突の解決は明示的な引数のみに限られます。ただし、戻り値の型がデフォルト式由来の型パラメータを参照するのは許されます(ジェネリックな型のイニシャライザや enum の case を表現するうえで必要なため)。
  • デフォルト式から推論される型パラメータと、そこから推論できない型パラメータとを結ぶ same-type 制約が存在しないこと。たとえば <T: Collection, U>(_: T = [...], _: U) where T.Element == U は不可ですが、<K: Collection, V>(_: [(K, V?)] = ...) where K.Element == V は同じ一つのデフォルト式から両方を推論できるので許可されます。
  • デフォルト式の型が、推論対象の型パラメータに課された適合・レイアウトなどの要件をすべて満たすこと。

呼び出し側での振る舞い

デフォルト値付きの引数が省略された場合、型チェッカはデフォルト式の型とパラメータ型の間に変換制約を張ります。この制約を通じて、戻り値の型との整合性も保証されます。

func compute<T: Collection>(initialValues: T = [0, 1, 2, 3]) -> T {
  ...
}

let r1: Array<Int>   = compute() // OK。両者とも Array<Int>
let r2: Array<Float> = compute() // error。デフォルト式は Array<Int> 型

Future Directions

本Proposalは「同じ型パラメータは一つのデフォルト式からしか推論しない」という制限を設けていますが、将来的には複数のデフォルト式を一緒に型チェックし、共通の型(type-join)で解決する方向への拡張も考えられます。たとえば test<T>(a: T = 42, b: T = 4.2) -> T を呼び出しごとに IntDouble かを使い分ける、といった挙動です。ただし、ユーザにとって直感的かどうかは自明ではなく、拡張するとしても別途議論が必要な speculative な見通しとして残されています。