Rustのモジュールを詳細に理解する(5) 可視性
概要: Rust2018以降、Rustのモジュールシステムは入門しやすく、かつマスターしにくくなりました。そこで本記事では、その両方を達成することを目指しました。
本記事は2017年に書いた記事 (https://qnighy.hatenablog.com/entry/2017/03/27/070000, https://qnighy.hatenablog.com/entry/2017/04/24/070000 ) と同じテーマで、Rust2018を考慮して改めてまとめ直したものです。
注意: 本記事中のサンプルコードの多くは、Rustの細かい仕様や実装を調べるためのものであり、実際に有用なプログラミングパターンであるとは限りません。また、実質的にバグに近い挙動を説明しているものもあるため、将来にわたって同じ動作であることはあまり保証できません。
可視性とは
可視性 (visibility) は、そのアイテムを名指ししていいかどうかのアクセス制御をする仕組みで、Rustにおける実装のカプセル化のために使われます。かつては pub
と未指定 (module private) の2種類でしたが、 RFC1422にてモジュール固有の可視性が実装されました。また、 RFC1560が実質的にRFC1422の事前準備としての役割を果たしているため、こちらも要チェックです。
可視性は名指しできるアイテムには基本的に付与されます。付与されないものとして以下があります。
- 実装
- 実装は型で索引されるため、可視性で管理するものではないという考えだと思われる。
- トレイトアイテムに可視性がないのも同様?
macro_rules!
によるマクロ- 伝統的に
#[macro_export]
の有無で制御されてきたため? - RFC1561では宣言マクロ2.0の一部として可視性の統一を目指す案も記載されている
- 伝統的に
逆に、アイテム以外に付与されるものとして以下があります。
- 固有実装の中身 (メソッド・関連関数・関連定数)
- 構造体・列挙型・共用体のフィールド
可視性の種類と構文
可視性の指定は意味的には2種類(+α)に分けられます。
- 公開。どこからでも参照できる。
- モジュール限定。特定モジュール以下から参照できる。
- 完全に不可視。アイテム自身からも見えないというのはおかしいので、ユーザーに見えるところでは出てこない。内部的に使われる。 (たとえば、可視性のjoin operationの単位元になっている)
単に pub
と書くと公開になります。そして、モジュール限定を指定するための記法がたくさんあります。
pub(in foo::bar::baz)
と書いた場合。この場合foo::bar::baz
以下が可視範囲となる。- 未指定の場合 (
fn f() {}
) →pub(in self)
と同じ。 pub(self)
の場合 →pub(in self)
と同じ。pub(super)
の場合 →pub(in super)
と同じ。pub(crate)
の場合 →pub(in crate)
と同じ。crate
の場合 →pub(in crate)
と同じ。RFC2126の一部だが、Rust2018までに安定化する必要はないと判断され今に至る。
Q&A
pub(in)
のin
はなんで必要なの? → 元々の提案・実装ではなかったのですが、タプル構造体のメンバーに対して使ったときの構文的な曖昧性を取り除くために追加されました。pub(self)
って要るの?→マクロなどで機械的に処理するときにself
を放り込めて便利だからだと言われています。
なお、 crate
による可視性指定もタプル構造体のメンバーに使ったときの構文的な曖昧性が指摘されています。
可視性チェック
あるアクセスが可視性を満たしているかどうかは「どのDef(或いはresolution)を」「どのモジュールから見るか」によって決定されます。
どのDefを……については、たとえば foo::bar::baz
を参照しようとしているならば、 foo
, foo::bar
, foo::bar::baz
にそれぞれアクセス権があるかどうかチェックされます。このときたとえば foo::bar
が use
に由来しているときは、その use
自身の可視性に基づいてチェックされます。そのため、 baz
自体が pub
指定でも、それにどういうパスでアクセスするかや、 bar
内で baz
がどう use
されているかによってはアクセスできない場合もあります。
このこと自体は割と当たり前の挙動です。 use
は単に外部の名前をスコープに置くための便宜的な用途でも使いますが、それが外から見えてしまっては困るからです。
また、可視性を判定するときは use
は無視して、元々あったモジュールの親子関係(=モジュール木)に基づいて判定します。そのため、この判定はモジュールの親リンクを辿るだけで可能です。
可視性と use
可視性は use
の挙動にも影響を与えます。そのため、インポートを解決してから全ての可視性をチェックするわけではなく、インポート解決とuse
の可視性判定は同時進行します。
use
は複数の名前空間にまたがって処理されるので、個々の名前空間ではインポートの失敗が許容されています。
たとえば、 pub use foo::bar;
の場合、各名前空間ごとの処理では以下のような扱いになります。 (すでに foo
までは解決できている前提)
foo::bar
にアクセスできない場合は、その名前空間ではインポートしない。foo::bar
が存在しない場合と同じ扱い
foo::bar
にアクセスできるが、その可視性がpub
よりも弱い (e.g.crate
) 場合は、弱いほうの可視性でインポートする。foo::bar
にアクセス可能で、その可視性が十分な場合は、pub
でインポート (再エクスポート) する。
その上で、全体としては指定した可視性 (この場合は pub
) でインポートできたものが1つ以上ないとエラーになります。ただし、globインポートの場合はいくつか例外があり、たとえば何もインポートしなかった場合はエラーにはなりません。
mod m0 { pub fn f() {} mod f {} } mod m1 { fn f() {} pub mod f {} } use m0::f; // 値名前空間でしかインポートしない use m1::f; // 型名前空間でしかインポートしない mod m2 { pub(crate) mod m3 { pub(super) fn f() {} pub mod f { pub const X: i32 = 42; } } pub use m3::f; // 関数fをpub(self)で、モジュールfをpubでインポートする fn g() { f(); } } fn g() { m2::f::X; } mod m4 { // ここに例えば fn foo() {} と書くと下のインポートがエラーになる } pub use m4::*; // 何もインポートしないのでエラーにならない
可視性自身のパス解決
pub(in)
構文があるため、可視性の中でもパス解決をする必要があります。この可視性のパス解決はresolve_visibility
という専用のメソッドで行われています。
pub(in)
のパスは基本的には use
と同じ挙動をします。つまり、Rust2015モードではデフォルト絶対パスです。その他に以下のような特徴があります。
- 絶対パス・相対パス以外は未対応。つまり、
pub(in self::foo::bar)
は可能だが、pub(in foo::bar)
をRust2018モードで行うとエラーになる。 use
と異なり、当該パスのインポートが解決されるまで待つ仕組みは実装されていないらしい (issueにした)
use
以外の可視性検査
use
は上記のように名前解決と密接に関わっていますが、それ以外の可視性は以下のタイミングで検査されます。
private-in-public
private-in-publicはRFC0136の通称です。 (use
の可視性に関する規定もここに含まれていますが、これは今となっては扱いがかなり違うので本節の対象外とします)
RFC0136は、公開APIの型も公開されている必要があるという規定です。たとえば、
- 関数が
pub
なら、その引数や戻り値型もpub
である必要がある - 構造体フィールドが
pub
なら、その型もpub
である必要がある - トレイト実装の全ての入力型が
pub
なら、その関連型もpub
である必要がある
などの規定があります。
struct A; pub fn f(_: A) {} // error
pub trait Foo { type X; } struct A; type Unit = (); impl Foo for Unit { type X = A; // error }
逆にOKなものとしては、
- 構造体が
pub
でも、そのフィールドがpub
である必要はない - 関数が
pub
でも、その実装で使われているアイテムがpub
である必要はない
などがあります。
このような可視性は名前解決よりも後のフェーズで検査されます。
private-in-publicのlintへの緩和
RFC2145によりprivate-in-public規則が整理されました。
- 既存の規則がより明確に説明されました。
- 到達可能性に基づく新しい規則が提案されました。
- それにあわせて、既存のprivate-in-publicチェックをlintに緩和することが提案されました。
到達可能性とヴォルデモート型
現在のprivate-in-publicルールはヴォルデモート型 (Voldemort type) と呼ばれる種類のパターンを許容しています。
mod m1 { pub struct Voldemort; } // 外部クレートはVoldemortにアクセスする手段がない。 // しかし、Voldemort自体はpubなので、この関数は許されている pub fn you_know_who() -> m1::Voldemort { m1::Voldemort }
RFC2145で提案されている新しい規則では、ヴォルデモート型に対する対応も含まれます。
public/private dependencies
private-in-publicに関連して、RFC1977ではこれを依存関係に対して拡張することを提案しています。たとえば、 Cargo.toml
に以下のように書いたとします。
[package] name = "foo" # ... [dependencies] # fooは内部的な高速化のためにrayonを使うが、 # パラレルイテレーターのインターフェースを提供するわけではないのでprivate rayon = { version = "1.0.3", public = false } # fooの公開している型がSerialize/Deserializeを実装しているのでpublic serde = { version = "1.0.90", public = true }
このとき、 foo
の公開APIに rayon
の型が含まれていないか が追加で検査されます。
この場合、 foo
が rayon
のメジャーバージョンを上げても自身のメジャーバージョンを上げる必要はない一方、 serde
のメジャーバージョンを上げたときは(通常は)自身のメジャーバージョンも上げる必要があると解釈されます。 (ただし、semver compatibilityを機械的に調べる方法は今のところないので、これはインフォーマルにそう定められているだけといえる)
またこの情報を、三角依存関係を同じバージョンに解決するかどうかの判定にも使えないかと考えられています。たとえば上の状況で、さらに bar
が rayon
, serde
, foo
の全てに依存していたとします。このとき、 serde
は単一バージョンに解決する必要がある一方、 rayon
は単一バージョンに解決する必要はないことになります。
まとめ
可視性 (pub
) とインポート解決の挙動について説明しました。ここまでを理解すれば、モジュール構造の設計で(挙動の予測ができずに)困ることはほぼなくなるのではないでしょうか。また、それに関連して、名前解決以降のフェーズでの可視性検査も軽く説明しました。
インポートの解決までがようやく説明できたので、これを前提に次回はパス解決を説明を説明します。