Ease the transition to concise magic file strings
01 何が問題だったのか
Swift には、呼び出し位置のソースファイルパスを表す magic identifier #file があります。アサーションやログ出力のデフォルト引数などで広く使われてきましたが、フルパスはビルド環境ごとに異なりバイナリサイズを増やすうえに、開発者の作業ディレクトリ名などのプライバシー情報を漏らしてしまうという問題がありました。
そこで SE-0274 では、
- フルパスを得るための
#filePathを新たに導入し、 - 既存の
#fileの意味をModuleName/FileName.swiftという簡潔な文字列に変更する、 - デフォルト引数で
#fileを使う関数を、別の#filePath既定値の関数のラッパーにしてしまったようなケースを検出する警告を追加する、
という方針が採択されました。1 と 3 は Swift 5.3 で有効化されましたが、2 は staging flag の裏に隠したうえで、Swift 5.3 以降のどこかのリリースで既定の挙動にする計画でした。Swift 5.3 のサイクル全体を猶予期間として、次のような既存コードを
/// Prints the path to the file it was called from.
func printSourcePath(file: String = #file) { print(file) }
必要に応じて次のように書き換えてもらう想定だったのです。
/// Prints the path to the file it was called from.
func printSourcePath(file: String = #filePath) { print(file) }
しかし、SwiftNIO のようにソース配布されるライブラリでは、複数の Swift バージョンを同時にサポートする必要があり、#file から #filePath への移行を #if compiler(>=5.3) で条件分岐させたいという要求がありました。ところが、これが言語仕様上うまく書けないことが分かってきました。
まず、デフォルト引数における #file / #filePath の「呼び出し位置を指す」挙動は 推移的ではありません。そのため、次のように補助関数でラップしても目的は達成できず、printSourcePath が定義されたファイルのパスが出力されてしまいます。
/// Prints the path to the file it was called from.
/// - Warning: Actually prints the path to the file printSourcePath(file:) was
/// defined in instead!
func printSourcePath(file: String = filePathOrFile()) { print(file) }
#if compiler(>=5.3)
func filePathOrFile(_ value: String = #filePath) -> String { value }
#else
func filePathOrFile(_ value: String = #file) -> String { value }
#endif
また #if はデフォルト引数の内側に書くこともできません。#if は文や宣言全体を囲むものなので、次のような記述は構文エラーになります。
/// Prints the path to the file it was called from.
/// - Warning: Actually fails to compile with a syntax error!
func printSourcePath(file: String = #if compiler(>=5.3)
#filePath
#else
#file
#endif) { print(file) }
結局、条件コンパイルで関数定義そのものを二重化するくらいしか現実的な手段が残らず、ライブラリ作者たちにとってこの移行コストは過大でした。
fileprivate func _printSourcePathImpl(file: String) { print(file) }
#if compiler(>=5.3)
/// Prints the path to the file it was called from.
func printSourcePath(file: String = #filePath) { _printSourcePathImpl(file: file) }
#else
/// Prints the path to the file it was called from.
func printSourcePath(file: String = #file) { _printSourcePathImpl(file: file) }
#endif
「簡潔な file 文字列を既定にしつつ、フルパスが必要な呼び出し位置には #filePath を残す」というゴールは正しいものの、SE-0274 のスケジュールでは Swift 5.3 のタイミングで #file の意味を実質的に変えてしまうため、ソース互換性への影響が大きすぎたのです。
02 どのように解決されるのか
SE-0274 の方針を修正し、#file の意味変更を「source break を伴う将来の言語モード(仮に Swift 6 モードと呼ぶもの)」まで延期します。そのうえで、Swift 5.3 時点でも簡潔な file 文字列を使えるようにするため、新しい magic identifier #fileID を導入します。
3 つの magic identifier の役割
変更後は、次の 3 種類の magic identifier が併存します。
#filePath: コンパイラに渡されたフルパスを返します。従来の#fileと同じ挙動です。#fileID:ModuleName/FileName.swift形式の簡潔な文字列を返します。どの言語モードでもこの意味で、直ちに利用できます。#file: Swift 4 / 4.2 / 5 モードでは引き続き#filePathと同じ文字列を返します。将来の言語モードでは#fileIDと同じ文字列を返すようになり、そのモードでは#fileIDは deprecated になります。
つまり、Swift 5 モードのコードは今まで通り #file を使い続けて構いません。#file の意味が実際に変わるのは、ユーザーが自分のプロジェクトを将来の(source break を許容する)モードに切り替えたときだけです。既存のソース配布ライブラリは、Swift 5 モードに留まれば無修正でこれまでと同じ挙動を維持できます。
// Swift 5.3 以降、どの言語モードでも使える
func log(_ message: String, file: String = #fileID) {
print("[\(file)] \(message)")
}
log("hello")
// 出力例: [MyModule/Main.swift] hello
将来のモードで #file の意味が変わる際、モジュール境界を跨ぐ呼び出しでも齟齬が出ないよう、#file のデフォルト引数はライブラリ側のコンパイルモードに応じて解釈されます。Swift 5 モード以前でコンパイルされた呼び出し側が、将来モードでコンパイルされたライブラリを呼ぶ場合、そのライブラリ側の #file デフォルト引数は #fileID と同じ扱いになります。逆に、将来モードで書かれた呼び出し側が Swift 5 モードのライブラリを呼ぶ場合は、ライブラリ側の #file は #filePath と同じ扱いになります。
標準ライブラリとコンパイラ生成トラップ
assert / precondition / fatalError などの標準ライブラリのアサーション系関数のデフォルト引数は #fileID に切り替わります。force unwrap の失敗など、コンパイラが自動生成するトラップにファイル名が含まれる場合も、#fileID と同じ形式の文字列が使われるようになります。これにより、リリースビルドに載る文字列は簡潔になり、プライバシーの観点でも改善されます。
ミスマッチ警告の緩和
SE-0274 で導入された、デフォルト引数に #file / #filePath が取り違えて伝わってしまうケースの警告は、次のように整理されます。
#fileと#fileIDを互いに渡し合っても警告しません。- Swift 5 モード以前では、
#fileと#filePathを互いに渡し合っても警告しません。 - 将来モードでは、
#fileは#fileIDとのみ互換として扱われます。
Swift 5 モードで #fileID を受け取るパラメータに #file(= 実体は #filePath)を渡すと、厳密には挙動が異なり、#fileID が意図した簡潔な文字列ではなくフルパスが入ってしまいます。しかし、多くの場合それは「必要以上に長い文字列が出るだけ」で機能的には壊れないため、既存の #fileID アダプタをラップする関数に不要な警告を出してしまう弊害を避けるため、警告対象からは外されています。
API デザインガイドラインでの推奨
Swift API Design Guidelines には次の方針が追記されます。読者がデフォルト引数を選ぶときの指針になります。
- 本番環境で動く API では
#fileIDを優先する。文字列が短くなり、プライバシーも守られる。 - テストヘルパーやスクリプトのようにエンドユーザが動かさない API や、ファイル I/O に使う場合のように実際のフルパスが必要な場面では
#filePathを使う。 - Swift 5.2 以前とのソース互換を保ちたい場合のみ
#fileを使う。
選択の考え方
結果として、アプリやライブラリの作者は次のように選ぶことになります。新規コードでは基本的に #fileID(本番向け)か #filePath(フルパスが必要な場合)を選び、#file は旧 Swift との互換が必要な既存コードベースに限って使います。将来モードに移行したタイミングで #fileID は deprecated になりますが、その時点では #file がそのまま同じ役割を担うため、コード側で書き換える必要は生じません。