Swift Digest
SE-0451 | Swift Evolution

Raw identifiers

Proposal
SE-0451
Authors
Tony Allevato
Review Manager
Joe Groff
Status
Implemented (Swift 6.2)

01 何が問題だったのか

Swift の識別子には文法上の制約があり、使える文字は限られています。名前は短くシンプルに保ちたいという多くのケースではこれで十分ですが、実際には「自然な名前」が Swift の識別子ルールに収まらない場面が存在します。現状、そうした場面では名前を無理やり変形したり、余計な接頭辞を付けたりといった回避策に頼るしかなく、元の名前が持っていた情報が失われたり、コードが読みにくくなったりします。

テストを自然に記述したい

swift-testing では @Test("square returns x * x") のように、説明的なテスト名を属性の引数として渡せます。ところが属性に渡した説明はあくまで表示用で、別途テスト関数の識別子も用意しなければならず、同じ説明を二重に書く ことになります。

@Test("square returns x * x")
func squareIsXTimesX() {
  #expect(square(4) == 4 * 4)
}

これは単に冗長なだけでなく、ツールによって見えるテスト名が食い違う問題も生みます。テストレポートや IDE の階層表示には属性に書いた説明が使われますが、コンパイラやリンカ、デバッガ、インデックス、バックトレースなどの低レベルなツールでは Swift のシンボル名(関数名)しか見えません。

「属性だけで済ませる」案として @Test("...") { ... } のような trailing closure 形式も考えられますが、progressive disclosure を崩したり、クロージャとしての処理の違いが顔を出したりするため採用されませんでした。結局のところ、シンボル名そのものを説明的にできなければ、この不整合は解消できません。

数字始まりなどの自然な名前

名前が数値で始まるのが自然な場面もあります。たとえばカラーシステムで、色のバリアントが 50 / 100 / 200 のような数値で表されるケースです。enum で型安全に表現したいところですが、case 100 は識別子ルール違反なので、case _100 のように先頭にアンダースコアを付けたり、case variant100 のように接頭辞を足したり、case v50 のような意味不明な省略形にしたりする必要があります。

// どれも不自然
enum ColorVariant { case _50, _100, _200 }
enum ColorVariant { case variant50, variant100, variant200 }
enum ColorVariant { case v50, v100, v200 }

API ごとに別々の回避策が採られるため、読者はそのたびに規則を読み替えなければなりません。

コード生成と FFI

外部のデータ定義や他言語の API から Swift コードを生成する場合も同じ問題が起きます。元の名前が Swift の識別子として妥当でないことはしばしばあり、SF Symbols の 1.circle のように、そのままでは Swift で書けない名前も珍しくありません。生成ツールは「変換規則」を用意して元の名前を Swift 向けに変換しますが、変換後の名前が元データにも存在しうるため衝突回避のルールが複雑化し、利用者もその規則を覚える必要が出てきます。

モジュール名のスケール

Swift モジュールは単一のフラットな名前空間にあり、短く単純な名前を付ける文化が広がっていますが、大規模なモノレポや Bazel のような分散ビルド環境では衝突が起きやすく、SE-0339 の module aliasing を使っても衝突回避のために別名を考える負担が残ります。Bazel は myapp/extensions/widget/common/utils のようなビルドターゲット識別子から myapp_extensions_widget_common_utils といったモジュール名を機械的に導出しますが、この変換は不可逆で、元の階層構造が名前から読み取れません。利用者は import のたびに変換を頭の中で行う必要があり、ツールも逆変換ができません。

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

Swift の識別子構文を拡張し、バッククォートで囲むことで通常の識別子文字以外も含められる raw identifier(raw identifier)を導入します。これまでもキーワードを識別子として使うために `for` のようなバッククォート記法はありましたが、バッククォート内に書けるのは従来の識別子文字に限られていました。本提案ではこの制限を取り払い、空白や記号、数字始まりなど、より自由な文字列を識別子として扱えるようにします。

用語としては、従来の「キーワードをバッククォートで囲んだもの」を escaped identifier、「通常の識別子文字以外を含むもの」を raw identifier と区別します。いずれの場合も、バッククォートは区切り記号であって識別子自体の一部ではありません。func `for`() は名前 for の関数を定義し、func `with a space`() は名前 with a space の関数を定義します。

テスト名を自然に書く

swift-testing のテスト関数名を raw identifier で書けば、説明を一箇所にまとめられます。

@Test func `square returns x * x`() {
  #expect(square(4) == 4 * 4)
}

この一つの名前がテストレポート・デバッガ・インデックス・クラッシュログなど、あらゆる場所で使われます。テスト関数の名前は「説明そのもの」であり、実体としての呼び出し元はテストフレームワークだけなので、表現力のある命名を許しても問題は起きません。progressive disclosure の観点でも、通常の関数を書ける人が @Test を付けることでテストにでき、そこから raw identifier を覚えるだけで説明的な名前に到達できる、という自然な道筋になります。

数値始まりの識別子

カラーシステムの例は次のように素直に書けます。

enum ColorVariant {
  case `50`
  case `100`
  case `200`
}
let color = Color(hue: .red, variant: .`100`)

FFI やコード生成でも、元の名前をそのまま Swift の識別子に写像できます。

extension UIImage {
  static var `10.circle`: UIImage { ... }
}

使える文字

raw identifier には以下を 除く 任意の Unicode 文字を書けます。

  • バッククォート(`): 識別子を閉じるため。
  • バックスラッシュ(\): 将来のエスケープ記法のために予約。
  • CR(U+000D)/ LF(U+000A): 複数行にはできません。
  • NUL(U+0000)および文字列リテラルで禁止されている非印字 ASCII(U+0001...U+001F, U+007F)。

また、空白のみで構成される識別子は禁止されます(ここでの「空白」は Unicode の White_Space プロパティ)。先頭・末尾・途中に空白を含むことは許されます。

演算子文字との関係

演算子文字を含むことはできますが、演算子文字のみ で構成される raw identifier はパースエラーです。識別子でも演算子でもないものとして扱われ、将来この構文で演算子を参照できるようにする余地を残しています。

func + (lhs: Int, rhs: Int) -> Int    // ok
func `+` (lhs: Int, rhs: Int) -> Int  // error

let x = 1 + 2    // ok
let x = 1 `+` 2  // error

property wrapper との組み合わせ

property wrapper が付けられた `with a space` のような raw identifier のプロパティを、バッキングストレージや projected value として参照する場合、接頭辞の _ / $バッククォートの内側 に入れます。

struct UsesWrapper {
  @Wrapper var `with a space`: Int
}

print(x.`_with a space`)            // correct
doSomethingWith(x.`$with a space`)  // correct

print(x._`with a space`)            // error
doSomethingWith(x.$`with a space`)  // error

また、`$0` のように $ + 数字の raw identifier は通常の識別子として扱われ、無名クロージャ引数($0)の意味にはなりません。これにより、数値名のプロパティに対しても property wrapper の projected value にアクセスできます。

struct UsesWrapper {
  @Wrapper var `0`: Int

  func f() {
    let closure: (Int) -> Int = { _ in
      doSomethingWith(`$0`)  // `0` の projected value
      return $0              // 無名クロージャ引数
    }
  }
}

メンバアクセスでは必ずバッククォートが必要

escaped identifier では、文脈から曖昧でない場合に限りバッククォートを省略できました(Access.public など)。一方 raw identifier は、空白や記号などパースの区切りになりうる文字を含むため、どの文脈でも必ずバッククォートで囲む 必要があります。

struct S {
  var `with a space` = 0
}

S().with a space    // error: どこまでが名前か分からない
S().`with a space`  // correct

この規則のおかげで、raw identifier とタプルの要素インデックスが混同されることもありません。tuple.0`` は必ず「ラベル 0 の要素」、tuple.0 は必ず「インデックス 0 の要素」として解釈されます。

let x = (0, 1)
_ = x.`0`  // error(ラベルが無い)

let y = (5, `0`: 10)
let b = y.0    // 5
let c = y.`0`  // 10

Objective-C 互換性

raw identifier で名付けられた宣言を Objective-C に公開する場合、Objective-C として有効な名前を @objc(...) で明示的に与える必要があります。Swift 名が Objective-C 識別子として不正で、かつ明示名も指定されていない(または明示名自体が不正)場合はエラーになります。

@objc class `Class with a Space` {  // error
  @objc(someFunction) func `some function`() {}  // ok
  @objc(`not valid`) func myFunction() {}  // error
}

モジュール名

Clang の module map はすでに module "some/module/name" のように非識別子文字を含む名前を書けるため、Swift 側で import を raw identifier 対応するだけでそのまま取り込めます。

import `some/module/name`

// 曖昧さ回避で明示修飾する場合
`some/module/name`.SomeClass

Swift モジュールは「モジュール名 = .swiftmodule のファイル名」という前提があるため、ファイル名に使えない文字を直接モジュール名にはできません。その代わり、コンパイル時の -module-name は従来どおりファイルシステム互換の名前に限定し、-module-alias で指定するエイリアスに raw identifier を使えるようにします。利用者が書く名前(ソース上のモジュール名)と、物理的なファイル名・ABI 名を分離する仕組みです。ABI 名そのものをソース上の名前と一致させたい場合は -module-abi-name を raw identifier で渡せます。

シンボルマングリング

Swift のシンボルマングラはすでに非 ASCII 識別子文字を Punycode で扱えるようになっており、raw identifier にもそのまま適用されます。ただし、従来のマングラは「数字で始まる識別子」を想定していなかったため、数字始まりの raw identifier も Punycode エンコード対象として扱うよう改修されます。これにより FontWeight.100() のような宣言も正しく往復するマングリングが得られます。

ツールへの影響

SE-0275 のレビューで、空白などを含む識別子は IDE の扱いが難しくなるのではないかという懸念が挙げられました。現在では LSP の textDocument/selectionRange で「このカーソル位置の意味的に妥当な選択範囲」を返せるため、ダブルクリックで識別子全体を選ぶといった操作は言語サーバ側で自然にサポートできます。シンタックスハイライトも一行文字列リテラルと同程度の複雑さで実装できます。

Future Directions(見通し)

複数行の識別子や、バッククォート自体を含む識別子を書けるようにするために、raw string literal と同様の # 区切り構文を拡張する方向性が示されています。たとえば #`contains`some`backticks`# や、三連バッククォートで囲む複数行識別子などが構想されていますが、現時点で有力なユースケースが無いため本提案のスコープには含まれていません。

バックスラッシュ(\)は将来のエスケープ記法のために予約されており、現時点では raw identifier 内での使用を禁止するにとどまっています。これはあくまで speculative な見通しで、実現が約束されているものではありません。