Module selectors for name disambiguation
01 何が問題だったのか
Swift では、ソースコード中のある名前がどの宣言を指すかを名前解決で決めます。これには、a.b のように左辺の型のメンバーだけを見る qualified lookup と、囲んでいるスコープを順にたどっていく unqualified lookup の2種類があります。unqualified lookup ではローカル宣言、引数、エンクロージング型のメンバー、トップレベルの宣言、インポート済みのモジュール、最後にモジュール名そのもの、という順で「もっとも近いスコープにある名前」が選ばれます。
このルールは普段の読み書きには自然で便利な一方、名前の衝突や曖昧さが避けられない状況がいくつかあり、それを surface syntax で解消する手段が今まで存在しませんでした。
モジュール名がシャドーされる
トップレベルの宣言は通常 ModuleName.Name の形でモジュール名を付けて修飾できます。しかし、モジュール内に同名の型があるとモジュール名そのものがシャドーされ、修飾が効かなくなります。
// モジュール RocketEngine
public struct RocketEngine { ... }
public struct Fuel { ... }
// 別のモジュール
import RocketEngine
_ = RocketEngine.Fuel() // RocketEngine.RocketEngine のネスト型 Fuel を探してしまう
これは特殊な例に見えますが、実は XCTest モジュールに XCTest クラス、Observation モジュールに Observable 型のように、モジュールと同名の型を持つ例は珍しくありません。回避するには、モジュールと型に違う名前を付けるしかありませんでした。
エクステンションのメンバーが解決不能に曖昧になる
複数のモジュールが同じ型に同名のエクステンションのメンバーを追加した場合、ABI のレベルではマングルされた名前にモジュール名が含まれているため区別できますが、surface syntax の側にはこれを書き分ける構文がありません。開発者は import を調整して、片方のメンバーだけが見えるようにするしかありませんでした。
マクロをモジュール名で修飾できない
マクロ展開の構文は単一の識別子しか許さず、続く . はマクロのメンバーアクセスとして解釈されます。そのため、マクロをモジュール名で修飾する手段が存在しませんでした。
module interface での自動生成が破綻する
これらの問題は、コンパイラが宣言を「確実に」参照するコードを生成しなければならない module interface の生成で特に深刻です。実際の Swift コンパイラは可能な範囲でモジュール名による修飾を試みますが、シャドーイングや名前衝突を完全に解消できず、複数の隠しフラグでのワークアラウンドに頼っているのが現状です。
これらは module interface の問題に見えがちですが、根本にあるのは「人間が書くコードでも同じ問題が起こる」という点です。surface syntax として、機械にも人間にも使えるあいまいさ解消の構文が必要でした。
02 どのように解決されるのか
名前の前に「どのモジュールから来た宣言か」を明示する module selector という新しい構文を導入します。<ModuleName>:: の形で、宣言を参照するあらゆる識別子の直前に置けます。
_ = RocketEngine::Fuel() // RocketEngine モジュール内の Fuel を選ぶ。
// 同名の Fuel が他にあっても無視される
:: という綴りは Swift では未使用で、カスタム演算子としても使えないため曖昧になりません。C++ や Rust などで広く使われている記法を踏襲しています。
unqualified lookup のスコープにも影響する
module selector を unqualified lookup で使うと、ローカル宣言・パラメータ・エンクロージング型などの中間スコープを飛ばして、いきなりモジュールのトップレベルから探し始めます。これにより、ローカル変数や型メンバーがトップレベル宣言をシャドーしている状況でも、確実にトップレベルの宣言を選べます。
// モジュール NASA
struct Scrubber { ... }
struct LifeSupport {
struct Scrubber { ... }
}
extension LifeSupport {
// トップレベルの Scrubber を返す
func makeMissionScrubber() -> NASA::Scrubber { ... }
}
qualified lookup でも使える
module selector はメンバーアクセスの途中でも使えて、「このメンバーはどのモジュールのエクステンションで定義されたものか」を指定できます。これがエクステンションのメンバーの曖昧さを解消する手段になります。
// モジュール IonThruster
extension Spacecraft {
public struct Engine { ... }
}
// モジュール RocketEngine
extension Spacecraft {
public struct Engine { ... }
}
// モジュール NASA
import IonThruster
import RocketEngine
func makeIonThruster() -> Spacecraft.IonThruster::Engine { ... }
なお、Mission.NASA::Booster.Exhaust と書いたときに module selector が修飾するのは、すぐ右隣の Booster だけです。Mission や Exhaust には影響しません。視覚的には (Mission.NASA) :: (Booster.Exhaust) のように見えてしまいますが、実際には Mission . (NASA::Booster) . Exhaust を意味します。
新しい宣言の名前には付けられない
module selector は「他で定義されている宣言を参照する」場面でのみ使えます。新しい宣言の名前として書くことはできません。
struct NASA::Scrubber { // 不可。新しい宣言は常にカレントモジュールのもの
...
}
マクロ展開・キーパス・属性などにも適用可能
module selector は、型・式・implicit member 式・マクロ展開・キーパスのコンポーネント・演算子の引数ラベル・init・明示的なメンバーアクセス・属性名・enum case パターンなど、宣言を名前で参照するほぼあらゆる位置に書けます。マクロをモジュールで修飾できなかった問題もこれで解消されます。
属性名に module selector を付けたものは、常にカスタム属性として扱われます。組み込み属性はどのモジュールにも属さない扱いなので、@Swift::available(...) のように書くと組み込みの @available ではなくカスタム属性として解釈されてしまうため不可です。
識別子パターンには付けられない
if let x のような、既存の変数のシャドーを宣言する短縮構文では、新しいローカル束縛を作っているため module selector は使えません。明示的に初期化式を書けば、その式の側で module selector を使えます。
if let NASA::rocket { ... } // 不可
if let rocket = NASA::rocket { ... } // OK
Task { [NASA::rocket] in ... } // 不可
Task { [rocket = NASA::rocket] in ... } // OK
キーワードも識別子として書ける
module selector の右に来るトークンがキーワードでも、原則として通常の識別子として扱われます。init や subscript、deinit のように特別な意味を持つ場合だけは、文脈に応じてその意味で扱われます。
print(default) // 不可。'default' はキーワードでバッククォートが必要
print(NASA.default) // OK(SE-0071 による)
print(NASA::default) // OK(本提案による)
scoped import でも使える
import struct などの scoped import で、import path を . で書く代わりに module selector を使えます。サブモジュールの指定はできず、サブモジュール内の宣言を取りたい場合は従来通り . 区切りで書きます。
import struct Foundation::Date // OK
:: の前後の空白
:: の左の識別子と :: の間には改行を含む任意の空白を入れられますが、:: と右の識別子の間に改行を入れることはできません。これは、let x = NASA:: の次の行に if x { ... } のような文が来たときに if を識別子として解釈してしまわないようにするためです。
NationalAeronauticsAndSpaceAdministration::
RocketEngine // 不可
NationalAeronauticsAndSpaceAdministration
::RocketEngine // OK
解決時の振る舞い
module selector が付いた参照は、指定されたモジュール内で宣言された、または再エクスポートされた宣言だけが候補になります。再エクスポート元も含めるので、たとえば Foundation を再エクスポートしている AppKit を経由して AppKit::NSString と書いても、もとの Foundation.NSString を取れます。これにより、ある型を別のモジュールに「持ち上げる」リファクタリングがソース互換を壊しにくくなります。
一方で、module selector はあくまで「他の候補を排除する」ための道具です。アクセス制御や未 import などで本来見えない宣言を、module selector で無理やり参照することはできません。
また、ジェネリックパラメータのメンバー型に対しては module selector を使えません。あるジェネリックパラメータが2つのプロトコルに適合し、両者に同名の関連型がある場合、そのメンバー型はその両方を同時に指す概念上の存在なので、モジュールで片方だけを選ぶこと自体に意味がないためです。
採用時の注意点
古いコンパイラは module selector を含むソースをパースできません。パッケージ作者は機能を使うなら tools-version を上げる必要があり、inlinable なコードを書く場合は後方互換性とのバランスを考える必要があります。
新しいコンパイラが module interface に module selector を出力するようになると、古いコンパイラはその interface を読めなくなります。Swift は module interface の後方互換性を保証していないので致命的ではありませんが、ABI 安定なモジュールについては、機能を有効化した interface を出力するかをオプトインにする段階的な運用が想定されています。
Future Directions(今後の見通し)
提案では今回のスコープ外として、次の方向性が示されています。いずれも実現を約束するものではなく、構文だけが将来の拡張のために予約されています。
- カレントモジュールを指す特別な綴り:
Self::や_::、*::、::のような綴りで「モジュールは限定しないがトップレベルから探し始める」ことを表す構文を追加する余地が残されています。 - subscript の曖昧さ解消:
myArray.Swift::[myIndex]のような書き方で subscript にも module selector を付けられるようにする方向性。 - 適合(conformance)の曖昧さ解消:retroactive な適合がモジュールごとに別物として ABI 上区別される一方、surface syntax にそれを書き分ける場所がない問題。module selector とは別の構文で扱う必要がありそうです。
- 競合するプロトコル要件の選択:同名の要件を持つ2つのプロトコルに適合する型に対し、
myTechnician.Employable::fire()のような形でどちらの要件を呼ぶかを指定する用途。ただし、ここで::の左にプロトコル名を許すと再びシャドーイングの問題が戻ってくるため、(myTechnician as some Employable).fire()のような別構文での解決が示唆されています。 - デフォルト実装の選択:プロトコル要件のデフォルト実装を、適合側のウィットネスを飛ばして呼ぶ用途。
superに近い別構文での解決が示唆されています。