Swift Digest
SE-0444 | Swift Evolution

Member import visibility

Proposal
SE-0444
Authors
Allan Shortlidge
Review Manager
Becca Royal-Gordon
Status
Implemented (Swift 6.1)

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.swiftRecipeKit だけを使っているとします。

// 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 も同じく Stringparse() を生やしていたとします。

// 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 属性が用いられることがあります。再エクスポートは推移的にも働くため、AB を、BC を再エクスポートしていれば、import A だけで A / B / C の宣言が見えます。

なお、コンパイラが暗黙にインポートする標準ライブラリや「現在ビルド中のモジュール自身」も可視モジュールに含まれます。その副作用として、自分のモジュールが(どこかのソースファイルで)再エクスポートしているモジュールは、自分のモジュールのどのソースファイルからも見えることになります。

挙動の例

先ほどの main.swiftparse() の例では、MemberImportVisibility を有効にすると、main.swiftGroceryKit をインポートしていない以上、GroceryKitparse() は候補に上がらなくなり、曖昧性は発生しなくなります。逆に、従来は推移的インポートだけに頼って使えていたメンバーは使えなくなります。

// 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 として挙げられています。