Swift Digest
SE-0275 | Swift Evolution

Allow more characters (like whitespaces and punctuations) for escaped identifiers

Proposal
SE-0275
Authors
Alfredo Delli Bovi
Review Manager
Joe Groff
Status
Rejected

01 何が問題だったのか

Swift では、予約語と同じ綴りの識別子を使いたい場合などに、識別子をバッククォートで囲むエスケープ記法が用意されています。しかし従来の文法では、エスケープした場合でも識別子として使える文字は通常の識別子と同じ範囲に制限されており、空白や句読点、記号などを含めることはできませんでした。また、数字で始まる識別子も許されていませんでした。

この制約は、特に次のような場面で不便を生みます。

テストメソッドの可読性

テストの目的を表すメソッド名は、どうしても長く・説明的になりがちです。しかし空白や句読点が使えないため、以下のように camelCasesnake_case を混ぜて無理やり読みやすくするしかありません。

func `test validation should succeed when input is less than ten`() // 現状は不可
// 代替:
func testValidationShouldSucceedWhenInputIsLessThanTen()
func test_Validation_Should_Succeed_When_Input_Is_Less_Than_Ten()
func test_validationShouldSucceed_whenInputIs_lessThanTen()

結果として、自然言語でテスト名を書きたいプロジェクトは Swift 自身のメソッド宣言ではなく、Quick のような外部テストフレームワークに頼ることが多くなっています。F# や Kotlin など他のモダンな言語では、エスケープ識別子に空白や句読点を含められるようになっており、テストランナーやレポートツールでも標準的に扱われています。

数字で始まる識別子

識別子には数字(0-9)を含められますが、先頭に置くことはできません。そのため、次のようにバージョン番号や HTTP ステータスコードを case 名にしたいケースが書けませんでした。

enum Version {
    case `1`   // 現状は不可
    case `1.2` // 現状は不可
}
enum HTTPStatus {
    case `300` // 現状は不可
}

コード生成ツールとの相性

コード生成ツールは、元の名前に含まれる Swift の識別子として不正な文字を何らかの形でマングリングしなければなりません。たとえば R.swift のようなアセット名を型付き値に変換するツールでは、Apple の SF Symbols にある 10_circle のような名前を _10_circle / ten_circle / _circle のいずれかに変形せざるを得ず、元のアセット名との 1 対 1 対応が崩れて検索性が落ちます。

非英語圏の自然な綴り

フランス語などアポストロフィやハイフンを多用する言語では、それらをエスケープ識別子の中で使えないことで、元の単語から離れた読みにくい綴りに置き換えることになってしまいます。

このように、エスケープ識別子で使える文字が通常の識別子と同じ範囲に限定されていることが、テストの表現力、コード生成、非英語識別子など複数の場面で一貫して問題になっていました。

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

エスケープ識別子の文法を拡張し、バッククォートで囲まれた中には、改行系の制御文字とバッククォート自身を除くあらゆる Unicode スカラ値を書けるようにすることが提案されていました。これにより、プロパティ・メソッド・型など、識別子が登場するあらゆる場所で空白や句読点を含む名前を宣言・参照できるようになります。

なお、本 Proposal はレビューの結果 Rejected となっており、以下の内容は「もし採用されていた場合にどう解決されるはずだったか」を示すものです。実際の Swift には取り込まれていません。

宣言と参照

宣言側は従来と同じバッククォート構文を使い、中身の文字集合だけが広がります。

func `test validation should succeed when input is less than ten`()
var `some var` = 0

参照側も同様にバッククォートで囲みます。

`test validation should succeed when input is less than ten`()
foo.`property with space`

意味論の保存

エスケープ識別子は、原則としてバッククォートを外した通常の識別子が持つ意味をそのまま引き継ぎます。したがって既存の意味論的制約もそのまま適用されます。

  • $ で始まる名前はコンパイラ予約であり、`$identifierNames` のような宣言はこれまでどおりエラーになります。
  • バッククォートなしでそのまま演算子として書ける綴りをエスケープした場合は、演算子として扱われ、演算子の意味論に従います。一方、演算子として解釈できない綴り(たとえば空白を含むもの)はメソッド等として扱われます。
static func `+`(lhs: Int, rhs: Int) -> Int // 演算子
func `test +`()                            // 演算子ではなく、ただのメソッド

この拡張により、これまで書けなかった演算子への参照も可能になります。

let add = Int.`+` // 現状は不可

文法の変更

文法としては、従来の

identifier → ` identifier-head identifier-characters opt `

を次のように置き換えます。

identifier → ` escaped-identifier `
escaped-identifier -> U+000A (line feed), U+000B (vertical tab), U+000C (form feed), U+000D (carriage return), U+0085 (next line), U+2028 (line separator), U+2029 (paragraph separator), U+0060 (back-tick) 以外のあらゆる Unicode スカラ値

つまり、改行系とバッククォート自身だけを禁止し、それ以外は(句読点・記号・絵文字なども含め)すべて許容します。

Objective-C との相互運用

Objective-C は Swift ほど広い Unicode スカラを識別子として受け付けません。Objective-C に公開するエスケープ識別子に未サポートの文字が含まれる場合は、既存の @objc 属性を使って Objective-C 側の名前を別途指定します。

@objc(sanitizedName)

Rejected の位置づけ

本 Proposal は最終的に Rejected となっています。したがって、現行の Swift でこれらの記法を書くことはできず、エスケープ識別子に使える文字は引き続き通常の識別子と同じ範囲に限定されます。テスト名の可読性やコード生成との相性といった課題は、この Proposal とは別の方法で扱われる必要があります。

今後の展望

Proposal には、今回のスコープ外として次の方向性も speculative に挙げられていました。いずれも将来の検討対象であり、実現が約束されているわけではありません。

  • 改行やバッククォート自身を含めたい場合、raw string literal に似た # 付きの区切りを使う拡張。

    func #`this has a ` back-tick`#()
    func ###`this has a
    new line`###()
    
  • <>. のような文字が識別子に入れるようになると、文字列からの型取得 API(仮に typeByName("Foo<Int>.Bar") のようなもの)と綴りが衝突する可能性があるため、そうした API を導入する際はバッククォートを文字列内の区切りとして使うことで曖昧さを解決する、といった設計余地。