Rustのモジュールを詳細に理解する(5) 可視性

概要: Rust2018以降、Rustのモジュールシステムは入門しやすく、かつマスターしにくくなりました。そこで本記事では、その両方を達成することを目指しました。

  1. モジュール入門
  2. 名前解決の役割と用語
  3. モジュールグラフの構造
  4. インポート解決
  5. 可視性
  6. パス解決
  7. 名前解決とマクロ

本記事は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::baruse に由来しているときは、その 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 の公開APIrayon の型が含まれていないか が追加で検査されます。

この場合、 foorayon のメジャーバージョンを上げても自身のメジャーバージョンを上げる必要はない一方、 serde のメジャーバージョンを上げたときは(通常は)自身のメジャーバージョンも上げる必要があると解釈されます。 (ただし、semver compatibilityを機械的に調べる方法は今のところないので、これはインフォーマルにそう定められているだけといえる)

またこの情報を、三角依存関係を同じバージョンに解決するかどうかの判定にも使えないかと考えられています。たとえば上の状況で、さらに barrayon, serde, foo の全てに依存していたとします。このとき、 serde は単一バージョンに解決する必要がある一方、 rayon は単一バージョンに解決する必要はないことになります。

まとめ

可視性 (pub) とインポート解決の挙動について説明しました。ここまでを理解すれば、モジュール構造の設計で(挙動の予測ができずに)困ることはほぼなくなるのではないでしょうか。また、それに関連して、名前解決以降のフェーズでの可視性検査も軽く説明しました。

インポートの解決までがようやく説明できたので、これを前提に次回はパス解決を説明を説明します。