Member import visibility
01 何が問題だったのか
Swift では、別モジュールで宣言された名前がソースファイル内で「スコープにある」と見なされるかどうかはインポートのルールで決まります。トップレベル宣言については直感どおりで、たとえば swift-algorithms パッケージの chain() を使うには、そのファイルに import Algorithms と書かないとコンパイラに見つけてもらえません。
// 'import Algorithms' が無いファイル
let chained = chain([1], [2]) // error: Cannot find 'chain' in scope
ところがメンバー宣言(型の中のメソッドや、extension で追加されたメソッドなど)は扱いが異なり、そのモジュールが推移的(transitive)にインポートされているだけでもスコープに入ってきてしまいます。推移的インポートには、同じターゲット内の別のソースファイルで直接インポートされている場合や、自分が直接依存しているモジュールがさらに依存しているモジュールなども含まれます。
これは仕様というよりはむしろ細かなバグに近い挙動で、普段のコードでは気づかずに済むことも多いのですが、extension のメンバーが絡むと驚きの原因になります。extension は「その型を持つモジュール」とは別のモジュールで書けるため、どこからともなく extension のメンバーがスコープに入ってきて衝突を起こすことがあるのです。
たとえばアプリ main.swift が RecipeKit だけを使っているとします。
// main.swift
import RecipeKit
let recipe = "2 slices of bread, 1.5 tbs peanut butter".parse()
// RecipeKit
public struct Recipe { /*...*/ }
extension String {
public func parse() -> Recipe?
}
後から別ファイルで GroceryKit を使い始めます。
// Groceries.swift
import GroceryKit
var groceries = GroceryList()
GroceryKit も同じく String に parse() を生やしていたとします。
// GroceryKit
public struct GroceryList { /*...*/ }
extension String {
public func parse() -> GroceryList?
}
すると、GroceryKit を インポートしていないはずの main.swift で突如コンパイルエラーが発生します。
// main.swift
import RecipeKit
let recipe = "2 slices of bread, 1.5 tbs peanut butter".parse()
// error: Ambiguous use of 'parse()'
このように、無関係に見える別ファイルのインポートが遠くのファイルの名前解決に影響してしまうため、ローカルに読み解くのが難しく、曖昧性エラーの出方も直感に反したものになります。トップレベル宣言では起きないことがメンバーでは起きてしまう、というこの非対称性が問題の本質です。
02 どのように解決されるのか
メンバー宣言の可視性ルールをトップレベル宣言と揃え、そのソースファイルから「見えている」モジュールが宣言したメンバーだけがスコープに入るようにします。言い換えると、あるメンバーを使うには、それを宣言しているモジュールをそのファイルで直接インポート(またはそれに準ずる形で可視化)していなければなりません。
この新しい挙動は、将来の言語モードで既定となるほか、それより前のモードでも upcoming feature flag MemberImportVisibility を有効にすることで先取りして使えます。
「可視なモジュール」の定義
あるソースファイルから可視なモジュールは、トップレベルの名前解決で従来から使われているルールと同じ集合になります。具体的には次のいずれかに当てはまるモジュールです。
- そのソースファイル内の
import文で直接インポートされているモジュール - そのファイルに対応する bridging header からインポートされているモジュール
- 上記のいずれかで直接インポートされたモジュールから 再エクスポート(re-export) されているモジュール
再エクスポートは「このモジュールを import したクライアントに別のモジュールも見えるようにする」仕組みです。Clang モジュールでは modulemap の export * などで広く使われています。Swift モジュール側には公式な再エクスポート構文はありませんが、コンパイラ内部で @_exported 属性が用いられることがあります。再エクスポートは推移的にも働くため、A が B を、B が C を再エクスポートしていれば、import A だけで A / B / C の宣言が見えます。
なお、コンパイラが暗黙にインポートする標準ライブラリや「現在ビルド中のモジュール自身」も可視モジュールに含まれます。その副作用として、自分のモジュールが(どこかのソースファイルで)再エクスポートしているモジュールは、自分のモジュールのどのソースファイルからも見えることになります。
挙動の例
先ほどの main.swift の parse() の例では、MemberImportVisibility を有効にすると、main.swift で GroceryKit をインポートしていない以上、GroceryKit の parse() は候補に上がらなくなり、曖昧性は発生しなくなります。逆に、従来は推移的インポートだけに頼って使えていたメンバーは使えなくなります。
// RecipeKit は別ファイルで import されているが、このファイルでは import していない
let recipe = "1 scoop ice cream, 1 tbs chocolate syrup".parse()
// error: instance method 'parse()' is inaccessible due to missing import of defining module 'RecipeKit'
// note: add import of module 'RecipeKit'
このようにエラーメッセージは「どのモジュールを import すれば解決するか」を指摘してくれ、fix-it として直接インポートの追加が提案されます。移行の際はこの fix-it を当てていくのが基本的な作業になります。
SE-0409 との関係と併用
import 宣言自体のアクセス修飾子(public import / internal import など)を扱う SE-0409 と本提案は補完関係にあります。SE-0409 はインポートの アクセスレベル を、本提案はインポートの スコープ(どのファイルから見えるか) を統制するもので、両方を組み合わせることで「他モジュールから持ち込まれる宣言」の露出をよりきめ細かくコントロールできます。
Future Directions
本提案で解消されない extension メンバーの曖昧性については、将来的に「どのモジュールの extension メンバーかを呼び出し側で明示する」構文が検討されています(たとえば "...".RecipeKit::parse() のような記法)。本提案はインポートの整備で曖昧性を 予防する ものなのに対し、こちらは残った曖昧性を 事後的に解消する 手段という位置づけです。いずれも speculative な方向性で、実現を約束するものではありません。
ほかにも、protocol の retroactive conformance の可視性に対する同様のルール導入や、演算子・precedence group の名前解決ルールの見直しなどが Future Directions として挙げられています。