Access-level modifiers on import declarations
01 何が問題だったのか
Swiftでは型や関数などの宣言に対して public / package / internal / fileprivate / private のアクセス修飾子を付けられますが、import 宣言自体 に対するアクセス修飾子は存在しませんでした。その結果、モジュールをインポートすると、そのモジュール由来の宣言はすべてそのソースファイル内で「公開扱い」され、ライブラリの public API のシグネチャにそのまま現れても何も警告されません。
import DatabaseAdapter
// DatabaseAdapter は本来ライブラリの実装詳細のつもりでも、
// このシグネチャから間接的にクライアントへ露出してしまう
public func publicFunc() -> DatabaseAdapter.Entry { ... }
このため、ライブラリ作者が「この依存モジュールは実装詳細としてだけ使いたい」と意図していても、誤って public な宣言のシグネチャから参照してしまうと、依存モジュールがクライアントに漏れ出してしまいます。これは dependency creep(依存の膨張)の温床になります。
さらに、依存関係が「公開」か「実装詳細」かをコンパイラが区別できないため、ライブラリのクライアントをビルドするとき、本来は必要ないはずの transitive な依存モジュールまで常にロードする必要があり、ビルドパフォーマンス面でも無駄が生じていました。
従来、この問題を部分的に回避する手段として非公式な @_implementationOnly import が使われてきましたが、独自の型検査ロジックを持ち、非 resilient モジュールや @testable import との組み合わせで実行時クラッシュを引き起こすなど、安定性に難がありました。宣言のアクセスレベルと同じ枠組みで、インポートの可視性を一貫して扱える公式の仕組みが求められていました。
02 どのように解決されるのか
import 宣言の前に、宣言と同じアクセス修飾子(public / package / internal / fileprivate / private)を書けるようにします。open はインポートでは使えません。インポートに付けたアクセスレベルは、そのソースファイルにおける「インポート先の宣言の可視性の上限」として機能し、従来のアクセスレベル型検査ロジックがそのまま適用されます。
internal import DatabaseAdapter
internal func internalFunc() -> DatabaseAdapter.Entry { ... } // OK
public func publicFunc() -> DatabaseAdapter.Entry { ... }
// error: function cannot be declared public because its result uses an internal type
各アクセスレベルの意味
public import: どの宣言のシグネチャからでも参照でき、クライアントからも見えます。package import: 同じパッケージ内のモジュールから見え、package以下の宣言のシグネチャでのみ参照できます。internal import: モジュール内からのみ見え、internal以下の宣言のシグネチャでのみ参照できます。fileprivate import/private import: いずれもインポートを書いたソースファイル内のみに閉じ、fileprivate/private宣言のシグネチャでのみ参照できます(インポートについてはこの2つの挙動は同じです)。
fileprivate import DatabaseAdapter
fileprivate func fileprivateFunc() -> DatabaseAdapter.Entry { ... } // OK
internal func internalFunc() -> DatabaseAdapter.Entry { ... }
// error: ... its return uses a fileprivate type
public func publicFunc(entry: DatabaseAdapter.Entry) { ... }
// error: ... its parameter uses a fileprivate type
public func useInBody() {
DatabaseAdapter.create() // OK(シグネチャではなく本体での参照)
}
@inlinable
public func useInInlinableBody() {
DatabaseAdapter.create()
// error: 'create()' is fileprivate and cannot be referenced from an '@inlinable' function
}
通常の関数本体からの参照には制約がかからない一方、@inlinable や @backDeployed の本体、デフォルト引数、@frozen struct のプロパティなど インライン化されうるコード からの参照はアクセスレベルで弾かれます。
@usableFromInline import
インライン化されうるコードから依存モジュールを参照したいが、宣言シグネチャでは公開したくない、という場合は @usableFromInline をインポートに付けられます。package または internal のインポートに対してのみ使用できます。
@usableFromInline package import UsableFromInlinePackageDependency
@usableFromInline internal import UsableFromInlineInternalDependency
@usableFromInline 付きのインポートは、シグネチャに対しては元のアクセスレベル(package / internal)として振る舞いつつ、インライン化されうるコード本体からの参照を許可します。クライアントからは可視として扱われます。
デフォルトのアクセスレベル
アクセス修飾子のない素の import のデフォルトは言語モードによって変わります。
- Swift 5 / Swift 6:
publicがデフォルト(ソース互換性のため、従来のimportと同じ挙動) - 将来の言語モード:
internalがデフォルト(宣言のデフォルトと揃え、依存の意図しない露出を防ぐ)
現時点でも upcoming feature flag InternalImportsByDefault を有効にすると、将来の挙動を先取りして internal デフォルトにできます。将来の言語モードに移行する際には、これまで暗黙に public として動いていたインポートへ public を明示的に付ける対応が必要になります。
推移的依存のロード
インポートのアクセスレベルは、モジュールの 推移的(transitive)依存 をクライアントがロードする必要があるかどうかの判断にも使われます。モジュール全体としては、同じ依存に対して複数ファイルから異なるアクセスレベルでインポートされている場合、最も緩いものが採用されます。
次のいずれにも当てはまらない依存は「隠し」可能で、推移的クライアントはロードしなくて済みます。
publicまたは@usableFromInlineの依存は常にロードが必要。- 非 resilient モジュールの依存は、型のストレージレイアウトの都合で常にロードが必要。
packageの依存は、クライアントが同じパッケージに属する場合はロードが必要。@testable importされている場合は、private/fileprivateを含め全依存をロードが必要。
依存が隠せる場合、モジュールインターフェイス(.swiftinterface)の配布も不要になります(実行用のバイナリは引き続き必要です)。これによりビルド時間の短縮と、実装詳細モジュールの配布回避が見込めます。
他の属性との関係
@_exportedはpublic import(または現行モードでpublicがデフォルトになっている素のimport)にのみ付けられます。@testable importでも、インポートに書いたアクセスレベルは上限として働きます。@testableで見えるようになるinternal宣言についても、この上限が適用されます。- 既存の
@_implementationOnly importはinternal import以下に置き換えることが推奨されます。新方式のほうが型検査が厳密で、非 resilient モジュールや@testableとの組み合わせで起きていた不安定さも解消されます。 - スコープ付きインポート(
import struct Foo.Barのような形)はアクセスレベルと独立に機能します。アクセスレベルはモジュール単位で、スコープは名前解決の範囲を絞るためのもので、両者を組み合わせて使えます。
今後の見通し
非 resilient モジュールでも推移的依存を隠せるようにする方向が議論されています。型のストレージレイアウト情報を別経路でクライアントに届ける仕組みが必要になるため本提案のスコープ外ですが、将来的にこの制約が緩和される可能性があります(speculativeであり、実現を約束するものではありません)。