概要: Rust2018以降、Rustのモジュールシステムは入門しやすく、かつマスターしにくくなりました。そこで本記事では、その両方を達成することを目指しました。
- モジュール入門
- 名前解決の役割と用語
- モジュールグラフの構造
- インポート解決
- 可視性
- パス解決
- 名前解決とマクロ
本記事は2017年に書いた記事 (https://qnighy.hatenablog.com/entry/2017/03/27/070000, https://qnighy.hatenablog.com/entry/2017/04/24/070000 ) と同じテーマで、Rust2018を考慮して改めてまとめ直したものです。
注意: 本記事中のサンプルコードの多くは、Rustの細かい仕様や実装を調べるためのものであり、実際に有用なプログラミングパターンであるとは限りません。また、実質的にバグに近い挙動を説明しているものもあるため、将来にわたって同じ動作であることはあまり保証できません。
仮想モジュール
globではない use は必ず「どのモジュールから」「何を」「何という名前で」インポートするかという形で表現されます。しかし use crate as foo; や use foo as bar; など単一セグメントからなるパスのインポートの場合は「どのモジュールから」に相当する適切なモジュールがない場合があります。そこで名前解決時は3つの仮想モジュールが用意されています。
CrateRootAndExternPrelude ... 「仮想Rust2015モード」における :: 開始パスで使われる。ルートモジュールとして振る舞うが、ルートにないものに関しては ExternPrelude にフォールバックする。
ExternPrelude ... Rust2018における :: 開始パスで使われる。外部クレート名を解決する。
CurrentScope ... :: 開始パス以外の始点。
ExternPrelude仮想モジュールの解決
::foo::bar はRust2015とRust2018で振る舞いが異なります。
- Rust2015では
crate::foo::bar と同じ。
- Rust2018では
foo クレート以下の bar と同じ。 (=ExternPreludeが使われる)
ではExternPrelude仮想モジュールが解決する「クレート名」とは何でしょうか。具体的には以下が使われます。
- rustcに
--extern として渡されたもの (=Cargo.toml の依存関係)
#![no_core] がない場合、 core
#![no_core], #![no_std] がない場合、 std
#![no_core], #![no_std] がなくRust2018モードでコンパイルされている場合、 meta
- クレートルートに
extern crate foo as bar; がある場合、これが導入した別名 (i.e. bar)
最後の規則のおかげで、以下のようなコードがRust2015/2018マイグレーションコードとして許されることになります。
extern crate serde_json as json;
mod m1 {
pub fn foo() {
let ((),) = ::json::from_str("[null]").unwrap();
}
}
これに関連して、 extern crate self as foo; という構文が導入されています。これは use crate as foo; と似ていますが、上記のようにExternPreludeに別名を導入する追加効果があります。
たとえば、 serde_derive が serde:: で始まるパスを内部的に生成するとします。これを serde 自身から使うには extern crate self as serde; としておけばいいわけです。
CrateRootAndExternPrelude仮想モジュールの解決
:: ではじまるパスは以下の条件で特殊な動作をします。
- グローバルRust2018モードである (=今コンパイルしようとしているクレートがRust2018)
- 当該パスがRust2015クレートに由来している
この場合は仮想Rust2015モードと呼ばれ、絶対パス(Rust2015)とExternPreludeパス(Rust2018)の両方に解決されます。そのための仮想モジュールがCrateRootAndExternPreludeです。
CurrentScope仮想モジュール
CurrentScopeは通常の方法でパスを解決します。 use が単一セグメントからなる単独インポートだった場合に使われます。たとえば、 use foo as bar; であれば、 CurrentScopeから foo を bar としてインポートするという形で表現します。
複数セグメントからなる場合、たとえば use foo::bar as baz; であれば foo から bar を baz としてインポートすると表記できるため使われません。
パス全体の解決
ここまで何度か述べたように、パスの中でも pub(in) と use は特殊でした。 pub(in) は全く別のメソッドでパス解決されますし、 use は仮想モジュールがからむのでやや特殊です。それ以外のパスは、主に resolve_path で解決されます。
パス解決の全体の流れはシンプルです。 foo::bar::baz を値として解決したい場合、まず foo を型名前空間で解決します。次に foo の直下で型名前空間の bar を解決します。最後に foo::bar の直下で値名前空間の baz を解決します。ポイントは3つです。
- 最後のセグメントは所望の名前空間で解決しますが、それ以外のセグメントは型名前空間で解決します。 (モジュール・型・トレイトであることが期待されるため)
- 最初のセグメントの解決は複雑です。
- 先頭に特殊な接頭辞がついている場合があります。
接頭辞つきのパスの解決
:: 接頭辞
:: についてはすでに説明した通り、Rust2015では絶対パス、Rust2018ではextern preludeパスとして解釈されます。
crate:: または $crate:: で始まるパスは絶対パスです。このとき、 crate/$crate というキーワードが由来するクレートの絶対パスとして解決されます。たとえば、futures-0.1の try_ready! マクロは以下のように書かれています。
#[macro_export]
macro_rules! try_ready {
($e:expr) => (match $e {
Ok($crate::Async::Ready(t)) => t,
Ok($crate::Async::NotReady) => return Ok($crate::Async::NotReady),
Err(e) => return Err(From::from(e)),
})
}
この $crate は必ずこのfuturesクレートのルートに展開されます。このため、他のクレートで try_ready! を呼び出しても正しく動作します。
$crate はRFC0453で導入されたマクロ専用のキーワードですが、 crate:: (RFC2126) も動作はほぼ同じです。こちらはマクロ以外でも使えます。
self:: または super:: で始まるパスは相対パスです。 self/super をこの意味で解釈するのには以下の条件が必要です。
- その
self/super がパスの末尾であってはいけない。
self はパスの先頭に置く必要がある。
super はパスの先頭か self/super の直後に置く必要がある。
let x = self::foo;
let x = self::super::foo;
let x = super::foo;
let x = super::super::foo;
self/super を計算するときは、かならず正規モジュールが結果になるように調整されることに注意してください。正規モジュールとは、クレートルートまたは mod で定義されたモジュールのことです。正規ではないモジュールとは、 enum, trait またはブロックのことです。正規モジュールに調整するにあたっては正規祖先 (自分自身を含め、祖先の中で最も近い正規モジュール) が使われます。
const X: i32 = 42;
fn main() {
const X: i32 = 53;
dbg!(self::X);
}
super:: は self::super:: の省略形だと考えて、
self:: = 現在のモジュールの正規祖先
::super = その親モジュールの正規祖先
という風に定義できます。クレートルートの super をとろうとするとエラーになります。
接頭辞のないパスの解決
接頭辞のないパスはグローバルかローカルのいずれかに解決されます。ここまでの議論では完全に無視していましたが、とうとうローカル束縛のことも考えてあげる必要が出てきます。(なお、「匿名モジュール(ブロック)内で定義されたアイテム」はこの意味ではグローバルです)
接頭辞のないパスの1セグメント目は以下の優先度で解決されます。
- 通常のレキシカルスコープのローカル定義またはグローバル定義 (同じ階層ではローカル定義優先)
- 衛生的レキシカルスコープのグローバル定義
- extern prelude
- ツール属性のためのモジュール
- 通常のプレリュード
通常のレキシカルスコープの解決
接頭辞のないパスはresolve_ident_in_lexical_scopeで解決されます。レキシカルスコープはモジュールよりも細かいリブ (rib) という階層で処理されます。
リブは名前空間別に別々に管理されており、値名前空間・型名前空間に対して登録されます。 (※マクロ用のリブはない) またそれとは別にラベルを管理するリブがあります。
たとえば、 fn やクロージャは引数束縛があるので値名前空間にリブを作ります。 let はlet束縛があるので値名前空間にリブを作ります。ジェネリクスは型名前空間にリブを作ります。
また、モジュール自身もリブです。
fn id<T>(x: T) {
let y = x;
y
}
レキシカルスコープで名前を解決するときは、今いる位置で有効なリブを下から上に遡って探索していきます。モジュールリブなら、さらにグローバルな名前の解決も試みます。そして、正規モジュールのリブに到達したら、そこで探索を終了します。
そのため、基本的には mod 内・その場所で有効な定義のうち、最も近いものに解決されることになります。 (シャドウイングの一般的な規則)
fn f(x: i32) {
let x: i32 = 42;
let x: i32 = 53;
{
let x: i32 = 64;
}
}
ただし、ここでいう「有効な定義」は字句的な範囲 (let の場合はその let の終わりまで) で判定され、キャプチャー可能性など意味的な条件は使われません。そのため、
fn x() {}
fn f(x: i32) {
fn g() {
x;
}
}
type T = i32;
fn g<T>() {
const X: Option<T> = None;
}
また、シャドウイングが別途禁止されている場合もあります。たとえば let 内で const と同名の束縛を導入すると定数パターンになり、 static と同名の束縛を導入するとエラーになります。
const X: i32 = 42;
static Y: i32 = 42;
fn main() {
let X = 42;
let Y = 42;
}
ローカル定義とグローバル定義の間にも、基本的には最近優先のルールが適用できます。
fn foo() {
let x = 42;
{
fn x() -> i32 { 42 }
dbg!(x() + 11);
}
}
fn bar() {
fn x() {}
{
let x = 42;
dbg!(x + 11);
}
}
ただし、グローバル定義のタイムトラベルに注意が必要です。
fn main() {
assert_eq!(f(), 42);
let f = || 53;
assert_eq!(f(), 53);
fn f() -> i32 { 42 }
}
このため、レキシカルスコープのシャドウイングの優先度を考えるときは、アイテム(fn や staticなどグローバルに置けるもの)はモジュール(匿名モジュールの場合はブロック)の先頭に移動したと思って考えるとわかりやすいです。
衛生的レキシカルスコープ
RFC1584の定める宣言マクロ2.0では、現在の macro_rules! よりも広範囲のマクロ衛生性をサポートしています。
#![feature(decl_macro)]
mod m1 {
const X: i32 = 42;
pub macro f() {
dbg!(X);
}
}
fn main() {
m1::f!();
}
この場合、マクロを展開すると以下のようになります。
mod m1 {
const X: i32 = 42;
pub macro f() {
dbg!(X);
}
}
fn main() {
dbg ! ( X );
}
衛生性抜きでは、この X は解決できません。しかし、この識別子 X はマクロ展開 [1] に由来しているので、さらなる探索が行われます。マクロ展開 [1] で使われたマクロは m1::f なので、 f が定義された時のスコープ m1 でも X の解決を試みます。
extern prelude フォールバック
祖先モジュールで #![no_implicit_prelude] が定義されていなければextern preludeへのフォールバックが調べられます。
Rust2018で単に std::iter::once のように書いたときに標準ライブラリが参照されるのはこの仕組みによるものです。 (ようやくここまで来た……)
ツール属性のためのモジュール
祖先モジュールで #![no_implicit_prelude] が定義されていなければ、既知のツール属性モジュールへのフォールバックが調べられます。
ツール属性とはRFC2103で規定されている、「コンパイラは知らないが周辺ツールが知っている属性名」を安全に導入する仕組みです。
たとえばrustfmtで特定のアイテムのフォーマットを抑止するには以下のようにしていました。
#[cfg_attr(rustfmt, rustfmt_skip)]
fn foo() {}
単に #[rustfmt_skip] と書けばよさそうに見えますが、RFC0572で規定されている通りコンパイラは前方互換性のために未知の属性をエラーにすることになっています。そのため rustfmt というrustfmt処理時しか有効にならないcfgで条件をつけてやる必要がありました。
RFC2103はこれを解決するために属性のモジュール化をするもので、これにより次のように書けるようになりました。
#[rustfmt::skip]
fn foo() {}
これは通常の名前解決の仕組みにある程度乗っかっています。この場合はマクロ名前空間で rustfmt::skip が解決されていることになります。
そして、同じくRFC0572に言及されているように、当面の実装ではツール名は固定です。現時点では rustfmt と clippy が既知のツール名として扱われています。
このため以下のような奇妙なコードのコンパイルが通ります。
use {clippy, rustfmt};
通常のプレリュード
祖先モジュールで #![no_implicit_prelude] が定義されていなければ、通常のプレリュードへのフォールバックが調べられます。
よく知られているようにRustの標準ライブラリにはpreludeモジュールがあり、この中にあるアイテムは明示的なインポートなしで使用できるようになっています。
これを実現するためにまず、構文解析のタイミングで以下のような特殊な use が埋め込まれます。
#[prelude_import]
use ::std::prelude::v1::*;
埋め込まれるモジュールは実際には条件によって異なります。
#![no_core] が指定されたときは、何も埋め込まれません。
#![no_core] ではないが #![no_std] が指定されたときは、以下の2通りのどちらかになります。
#![compiler_builtins] が指定されたときは、 extern crate core; #[prelude_import] use ::core::prelude::v1::*; が埋め込まれます。
#![compiler_builtins] が指定されなかったときは、 extern crate core; extern crate compiler_builtins; #[prelude_import] use ::core::prelude::v1::*; が埋め込まれます。
#![no_core] も #![no_std] も指定されなかったときは、 extern crate std; #[prelude_import] use ::std::prelude::v1::*; が埋め込まれます。
型相対パスを考慮したパス解決
ここまでで説明したのは型相対ではない場合のパスの解決でした。 resolve_qpath は、これに加えて型相対パスを考慮した解決を行います。
<T>::A::B の場合は何もせず、パス全体 (A::B) を未解決として返します。
<T as A::B>::C::D の場合は <>:: の直後まで (この場合は <T as A::B>::C) の解決を試み、解決できたところまでを返します。
A::B::C の場合は単に解決できたところまでを返します。
ただし、 A::B::C 形式のパスの解決に失敗した場合、プリミティブ型へのフォールバックが検討されます。
use std::i32;
fn main() {
let x: i32 = 42;
dbg!(i32::MAX);
dbg!(i32::max_value());
}
プリミティブ型へのフォールバックは、以下の条件で発生します。
- 先頭セグメントがプリミティブ型名と一致する
- 先頭セグメントを型名前空間で解決しようとしていた (=全体を型名前空間で解決しようとしていた or 長さ2以上のパスを解決しようとしていた)
- パスの全体または途中までが正規モジュールに解決された
この場合、通常の方法で解釈しても望みがないことは明らか(モジュールの :: は名前解決の時点で解決できるはず)なので、先頭セグメントをプリミティブ型名として読み替え、型相対パスとして解釈します。
スコープ内トレイトの列挙
インポート解決で得られた結果は、パス解決のほかに、スコープ内トレイトの列挙のためにも使われます。これはRustの以下の規則のために必要です。
<T>::A は、型 T 自身の固有実装のほか、スコープ内にあるトレイトの実装から探索される。
x.method() は、 x の(自動参照・自動参照外しの適用後を含む)型の固有実装またはスコープ内にあるトレイトの実装から探索される。
関連アイテムやメソッドの解決をスコープ内トレイトに限定することで、「依存ライブラリを増やしただけで勝手にメソッド名の解決順が変わる」というような不安定な挙動を極力抑える意図があるものと思われます。
「スコープ内」と言っているだけあって、これは通常のパスの最初のセグメントの解決とほぼ同じ挙動をします。具体的には以下の優先順位で探索が行われます。
- 現在実装中のトレイト
- レキシカルスコープ内のトレイト
- 衛生的レキシカルスコープ内のトレイト
- 通常のプレリュード内のトレイト
2から4はほぼ同じなので省きます。「現在実装中のトレイト」が探索されることで以下のようなコードが許容されています。
use std::fmt;
use std::num::NonZeroU32;
#[allow(non_camel_case_types)]
pub struct u32m1(NonZeroU32);
impl fmt::Debug for u32m1 {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
(self.0.get() - 1).fmt(f)
}
}
上のコードではu32m1 の Debug::fmt の実装を u32 の Debug::fmt に移譲しています。ここで .fmt() と書けるのは、実装中のトレイトが使われているおかげです。
ちなみにトレイト自身のデフォルト実装では、そもそもトレイトがスコープ内にあることが普通なのでこのような特別対応は必要ありません。ただし、それによって以下のようなコーナーケースは発生しているようです。
trait Foo {
fn foo(&self) {
mod m1 {
fn f() {
}
}
}
}
impl Foo for () {
fn foo(&self) {
mod m1 {
fn f() {
().foo();
}
}
}
}
まとめ
(5)でモジュールグラフが作られ、(6)でパスの解決ができたので、これで名前解決の担当範囲はおおよそ理解できたことになります。
ただし、名前解決とマクロの相互作用についてはきちんと扱っていなかったので、次回は最後にその話をして終わりにしたいと思います。