Swift Digest
SE-0090 | Swift Evolution

Remove .self and freely allow type references in expressions

Proposal
SE-0090
Authors
Joe Groff, Tanner Nelson
Review Manager
Chris Lattner
Status
Returned for revision

01 何が問題だったのか

Swiftでは、型そのものを値として扱う「メタタイプ(metatype)」オブジェクトを取得したいとき、T.self という特別なメンバにアクセスする必要がありました。これは型名 T を式の中でそのまま書くことが許されていないためで、文法上、型参照は原則として「コンストラクタ呼び出し T(x)」か「メンバアクセス T.x」の一部として現れる場合にしか許されません。

// メタタイプを取得したい場合
let t: Any.Type = Int.self          // OK
let u: Any.Type = Int               // Error: 式文脈で裸の型参照は書けない

// 構造体のインスタンス化やメンバアクセスの文脈ならそのまま書ける
_ = String(42)
_ = Int.max

.self が必要な理由

このような制約が設けられていたのは、Swiftの式文法にいくつかの曖昧性があるためです。

  • ジェネリック引数の < >: T<U>(x) は「ジェネリック型 T<U> のコンストラクタ呼び出し」とも「T < U > (x) という比較演算の連鎖」とも読めます。Swiftはパーサの先読みで「> の直後にくるトークン(disambiguating token)」を見て判定しており、このdisambiguating tokenの集合が ( . などごく一部に限られていました。そのため、式の末尾など多くの位置では T<U> を裸で書くと比較演算として解釈されてしまい、メタタイプとして扱うには T<U>.self と書かざるを得ませんでした。
  • 型シュガーと式の衝突: T?Optional<T>)、[T]Array<T>)、[T: U]Dictionary<T, U>)といった糖衣構文は、同じ形の式(オプショナルチェーン、配列リテラル、辞書リテラル)と衝突します。そこで式文脈では前者の解釈を禁止し、[Int].self のように .self を付けてはじめてメタタイプを指すものとして扱う、という運用になっていました。

.self のつらさ

型オブジェクトを扱う機会はSwiftでは少なくありません。Swiftのメタタイプは「first-class」で強く型付けされた値として扱え、class メソッドや static なプロトコル要件を通じて動的なディスパッチにも使えます。しかしそのたびに T.self と書かされるのは、他のC系言語と比べてもSwift固有の煩わしさで、「おまじない」めいた記法として批判されることがありました。型を多く扱うAPIでは .self がコード全体に散らばり、読みづらさの原因になります。

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

この提案は Returned for revision(差し戻し) のまま再提出されず、deferredなProposal群をまとめて処理する過程で最終的にクローズされました。そのため、.self は現在のSwiftにも残っており、メタタイプを取得したい場面では引き続き T.self と書く必要があります。

以下は、仮に採択されていた場合に導入される予定だった内容の要約です。

ジェネリック T<U> の曖昧性解消を広げる

式文脈で T<U> を裸で書けるように、パーサの先読み規則を拡張します。具体的には、「ジェネリック引数リストの可能性がある <...> の直後にあるトークン」として受理する集合(disambiguating tokens)を、次のように広げます。

  • 区切り記号: . , ; : ? } ] ( )
  • キーワード: is as
  • 空白で囲まれた二項演算子
  • 改行の直後にある任意のトークン

この拡張により、ジェネリック型をメタタイプとして扱いたい場面のほぼすべてで、裸の T<U> が書けるようになります。

// 採択時に書けるようになる予定だった例
let t = T<U>                 // 文末(改行)で終わるケース
let types = [T<U>, V<W>]     // 配列リテラルの要素
doStuff(withType: T<U>)      // 関数引数
condition ? T<U> : V<W>      // 三項演算子の枝
x as T<U>.Type               // キャストの右辺

一方で、わずかに曖昧性が残るケース(例えば a<b>+c のように空白を伴わない演算子)については、Swiftが要求する「二項演算子の前後は空白を揃える」という既存ルールで切り分けます。

a<b> + c   // (a<b>) + c  ジェネリック型として読む
a<b > +c   // (a<b) > (+c) 比較演算として読む

型シュガーは文脈的型付けで解決する

T?[T](T, U, ...)[T: U] については、型検査の文脈で解釈を切り替えます。

  • 期待される型がメタタイプ(Any.Type など)なら、型参照として解釈する。
  • 期待される型が ArrayLiteralConvertible / DictionaryLiteralConvertible(現行の ExpressibleByArrayLiteral / ExpressibleByDictionaryLiteral に相当)なら、リテラル式として解釈する。
func useType(_ type: Any.Type) {}
func useArray(_ array: [Any.Type]) {}
func useDictionary(_ dict: [Any.Type: Any.Type]) {}

useType(Int?)              // Optional<Int> というメタタイプを渡す
useType([Int])             // Array<Int> というメタタイプを渡す
useType([Int: String])     // Dictionary<Int, String> というメタタイプを渡す

useArray([Int])            // Int を含む配列として扱う
useDictionary([Int: String]) // Int => String の辞書として扱う

型文脈が得られない場合は、コンパイラが曖昧だとしてエラーを出します。利用者は型注釈や as 変換で意図を明示することで曖昧性を解消します。

let x = [Int]              // Error: Array(Int) か Array<Int> か決められない

let x1: Any.Type = [Int]   // メタタイプ Array<Int>
let x2: [Any.Type] = [Int] // Int を含む配列
let x3 = [Int as Any.Type] // as によって配列リテラルとして確定

print([Int] as Any.Type)   // メタタイプとして出力
print([Int] as [Any.Type]) // 配列として出力

なお、型シュガーによる型参照として解釈できるのは、要素が具体的な型参照である場合に限られます。メタタイプを保持した変数に対しては型参照の解釈は成立しません。

let int = Int
useType([int])   // Error: 型参照にならず、配列リテラルとしても型が合わない

let y = [int]    // OK: int を要素に持つ配列として評価される

.self メンバの削除

上記の曖昧性解消が整うと、.self がなくてもメタタイプを参照できるようになります。そこで、.self という特殊メンバそのものを言語から削除する予定でした。既存コードは、T.self をすべて T に書き換える単純な移行で対応できる想定です。

結果として現在どうなっているか

差し戻しを受けて再提出はされず、.self はSwiftに残り続けています。メタタイプを取得したい場面では、現行Swiftでも Int.self[String].self のように .self を書く必要があり、ジェネリック型 T<U> を式末尾などで裸のまま書くこともできません。この提案で指摘された「.self の煩わしさ」自体は、その後も個別の文法改善で少しずつ緩和されてきたものの、抜本的に解消する提案は今のところ採択されていません。