Swift Digest
SE-0243 | Swift Evolution

Integer-convertible character literals

Proposal
SE-0243
Authors
Diana Ma (“Taylor Swift”), Chris Lattner, John Holdsworth
Review Manager
Ben Cohen
Status
Rejected

01 何が問題だったのか

Swift では、1 文字を表すリテラルにも文字列と同じダブルクォート("a")を使います。そのため CharacterUnicode.Scalar の値を書くには、次のように毎回 as で型を明示するか、型注釈で文脈を与える必要があり、冗長でした。

let c1: Character = "f"
let s1 = "f" as Character

ダブルクォートは文字列にもリテラル一文字にも使われるため、ぱっと見たときに "f"String なのか Character なのかを見分けづらいという問題もあります。String は書記素クラスタ(extended grapheme cluster)の Collection なのに対し、Character はその要素であり、「コレクション」と「要素」が同じ見た目を共有しているのは不自然でもあります。

さらに深刻なのは、ASCII の値をバイト列として扱いたい場面です。C では 'a' がそのまま整数 97 として扱えますが、Swift では ASCII 文字から UInt8 を得るために UInt8(ascii: "a") のような呼び出しを書く必要があり、次のようにテーブルを書くと非常に冗長になります。

let hexcodes = [
    UInt8(ascii: "0"), UInt8(ascii: "1"), UInt8(ascii: "2"), UInt8(ascii: "3"),
    UInt8(ascii: "4"), UInt8(ascii: "5"), UInt8(ascii: "6"), UInt8(ascii: "7"),
    UInt8(ascii: "8"), UInt8(ascii: "9"), UInt8(ascii: "a"), UInt8(ascii: "b"),
    UInt8(ascii: "c"), UInt8(ascii: "d"), UInt8(ascii: "e"), UInt8(ascii: "f")
]

加えて、init(ascii:)UInt8 にしか用意されていません。C API との境界で現れがちな Int8char)や、UInt16 などの他の整数型に対しては同じ手段すら使えず、Int8(UInt8(ascii: "a")) のような二重のキャストが必要になります。

for scalar in int8buffer {
    switch scalar {
    case Int8(UInt8(ascii: "a")) ... Int8(UInt8(ascii: "f")):
        // lowercase hex letter
    case Int8(UInt8(ascii: "A")) ... Int8(UInt8(ascii: "F")):
        // uppercase hex letter
    case Int8(UInt8(ascii: "0")) ... Int8(UInt8(ascii: "9")):
        // hex digit
    default:
        // something else
    }
}

また、Unicode.Scalar 経由で整数に変換する方法はコンパイル時の値検証が効きません。let char: UInt8 = 1989 はコンパイルエラーになりますが、let char: UInt8 = .init(ascii: "߅") は実行時エラーとして初めて検出されます。

本質的に「テキストだが数値として扱いたい」ASCII 値を、Swift らしい簡潔さと安全性を保ったまま書ける仕組みがなく、バイト列処理のコードが必要以上に冗長かつ間違えやすい状態でした。

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

本提案は Rejected となったため、以下の内容は 採用されなかった設計案 です。ただし議論の出発点となった「1 文字のリテラルに独立した構文を与えたい」「ASCII 文字を整数リテラルとして書きたい」というアイデア自体は、その後の Swift における文字リテラルやバイト列まわりの議論の下敷きになりました。

提案されていた構文: シングルクォートリテラル

CharacterUnicode.Scalar、および「1 要素分」のテキストリテラルで表現できる型全般について、シングルクォート(')によるリテラル構文を導入する案でした。型推論の既定は Character とし、型注釈で他の型に解決することを意図しています。

let c = 'a'              // Character と推論される
let u: Unicode.Scalar = 'a'
let s: String = "a"      // String は従来どおりダブルクォート

ダブルクォートはあくまで String 用とし、CharacterUnicode.Scalar に対するダブルクォートリテラルは段階的に deprecated として移行する計画でした。

let c2 = 'f'              // preferred
let c1: Character = "f"   // deprecated

ASCII 範囲の整数リテラル化

さらに、整数型(UInt8, Int8, UInt16, …, Int)を ExpressibleByUnicodeScalarLiteral に適合させ、ASCII 範囲(U+0 ..< U+128)の Unicode scalar リテラルを整数リテラルとして扱えるようにすることを提案していました。範囲を ASCII に限定しているのは、任意の Unicode scalar を整数に暗黙変換するとエンコーディング由来のバグを生みやすいためです。

ABI の制約上、標準ライブラリ側で直接この適合を宣言できないため、実装だけを標準ライブラリに用意し、利用者側で次のように opt-in で適合を宣言する形を想定していました。

extension Int8: ExpressibleByUnicodeScalarLiteral { }

この仕組みにより、先の hex テーブルや switch 文は次のように書けるようになります。

let hexcodes: [UInt8] = [
    '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
    'a', 'b', 'c', 'd', 'e', 'f'
]

for scalar in int8buffer {
    switch scalar {
    case 'a' ... 'f':
        // lowercase hex letter
    case 'A' ... 'F':
        // uppercase hex letter
    case '0' ... '9':
        // hex digit
    default:
        // something else
    }
}

ASCII 範囲外を整数型に渡した場合の扱いは、型検査で検出できるケースはコンパイルエラー、実行時に評価されるケースは precondition 違反とする設計でした。

let u: Unicode.Scalar = '\u{FF}'
let i1: Int = '\u{FF}'                          // compile-time error
let i2: Int = .init(unicodeScalarLiteral: u)    // run-time error

型ごとに受け入れるリテラル種別は次のように整理されていました(* は当時の挙動から変わる部分を表します)。

UnicodeScalarLiteral ExtendedGraphemeClusterLiteral StringLiteral
UInt8, …, Int yes*(opt-in) no no
Unicode.Scalar yes no no
Character yes(継承) yes no
String yes yes yes
StaticString yes yes yes

シングルクォートを選んだ理由

シングルクォート構文は C / C++ / Objective-C / Java / Rust など多くの言語で「文字」のリテラルとして使われており、前例があります。Swift でもかつてシングルクォートは将来の用途のために予約されていましたが、複数行文字列は """、補間は通常のダブルクォート、raw 文字列は #"..."#、正規表現リテラルは /.../ と、他の用途はすでに別の構文に割り振られていました。そのため、1 文字リテラルにシングルクォートを充てるのが自然だという整理です。文字列(列)と文字(要素)で見た目を分けることで、意味の取り違えも減らせるという狙いもありました。

却下の結果とその後への含意

レビューの結果、本提案は Rejected となりました(Rationale)。このため、上記のシングルクォート構文も、整数型に対する ASCII 文字リテラル化も Swift には導入されていません。Character / Unicode.Scalar は引き続きダブルクォートリテラルで書き、ASCII バイトが欲しい場面では従来どおり UInt8(ascii:) などを使うことになります。

ただし、提案の背景にあった「バイト列処理をもっと書きやすくしたい」「Character とそれ以外を視覚的に区別したい」という課題自体は残っており、その後の議論(組み込み用途や低レベル文字列 API、将来的なリテラル設計)の中で引き続き参照される題材となっています。