Swift Digest
SE-0213 | Swift Evolution

Literal initialization via coercion

Proposal
SE-0213
Authors
Pavel Yaskevich
Review Manager
John McCall
Status
Implemented (Swift 5.0)

01 何が問題だったのか

Swift の T(literal) という式は、見た目には「リテラルを T 型に変換する」ように読めます。しかし従来の Swift では、これは「T 型のイニシャライザを探して呼び出す」という通常のイニシャライザ呼び出しとして扱われていました。

そのため、UInt32(42) のような式は次の手順で型検査されていました。

  1. 引数 42 を単体で型検査する。このとき 42 は型が決まっていないので、デフォルトのリテラル型(整数なら Int)として解釈される。
  2. 得られた Int を引数に取る UInt32 のイニシャライザを探して呼び出す。

この挙動は、リテラルをそのまま目的の型として扱うつもりの利用者の直感とずれており、いくつかの実害も生んでいました。

コンパイル時オーバーフロー

UInt64(0xffff_ffff_ffff_ffff) のようなコードは、UInt64 としては正常に表現できる値であるにもかかわらず、従来はコンパイルエラーになっていました。引数 0xffff_ffff_ffff_ffff がまず Int として解釈されようとし、Int の範囲を超えるためオーバーフローと判定されてしまうためです。

一方で、同じ値を 0xffff_ffff_ffff_ffff as UInt64 と書けば、UInt64 のリテラルとして直接解釈されるので問題なくコンパイルできます。意味的には同じはずの2つの書き方で、結果が食い違っていました。

実行時まで遅れるエラー

リテラル1文字から Character を作る Character("a") のような式も、従来はイニシャライザ呼び出しとして扱われていました。この場合、引数 "a" はまず String として解釈され、String を受け取る Character のフェイラブルイニシャライザが呼ばれます。そのため Character("ab") のように複数文字を渡しても、コンパイル時にはエラーにならず、実行時に nil やトラップで気付くことしかできませんでした。

"ab" as Character のようにリテラル経由の変換として扱えば、Character リテラルは1文字でなければならないという制約をコンパイル時に検査できるはずです。

型チェッカの負荷

リテラルが関わる初期化式では、候補となるイニシャライザをすべて並べて1つずつ試すという素朴な解決を行っていたため、複雑な式では型検査が重くなる原因にもなっていました。

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

T(literal) の形の式について、T が対応するリテラルプロトコルに適合していれば、イニシャライザ呼び出しではなく literal as T と同じ「リテラルを T 型として直接構築する」挙動に切り替える ようになりました。

具体的な規則は次の通りです。

  • 関数呼び出し式が A(B) の形(引数が1個で、ラベルなし)である。
  • A が型 T を表し、B がリテラル式である。
  • TB に対応するリテラルプロトコル(整数なら ExpressibleByIntegerLiteral、文字列なら ExpressibleByStringLiteral など)に適合している。

この3条件を満たす場合、A(B) はリテラルプロトコルの init を使って「B as A」と同等の式として構築されます。

効果

Swift 4 までコンパイルエラーだった次のコードが、意図通りに通るようになります。

let big = UInt64(0xffff_ffff_ffff_ffff) // UInt64 リテラルとして直接構築される

Character への変換では、実行時ではなくコンパイル時にエラーを検出できるようになります。

let c = Character("ab") // error: 1文字でないため Character リテラルとして不正

型検査も単純化されるため、リテラルを多用する複雑な式のコンパイル速度の改善にもつながります。

従来のイニシャライザ呼び出しを明示したい場合

新しい挙動を避けて、あくまで通常のイニシャライザを呼び出したい場合は、.init を明示的に書きます。

struct Q: ExpressibleByStringLiteral {
    var question: String

    init?(_ possibleQuestion: String) {
        return nil
    }

    init(stringLiteral str: String) {
        self.question = str
    }
}

_ = Q("ultimate question")      // "ultimate question" as Q と同等。question に文字列が入る
_ = Q.init("ultimate question") // 従来通りのイニシャライザ呼び出し。nil になる

Q のようにリテラルプロトコルへの適合と、同じ引数型を取るフェイラブルイニシャライザの両方を持つ型では、この変更によって Q("...") の結果が変わります。実際にはこうした定義は非常に稀で、互換性テストでも該当例は見つかっていませんが、影響を受ける場合は .init で書き分けることで従来の挙動を取り戻せます。