Rustのモジュールを詳細に理解する(2) 名前解決の役割と用語
概要: Rust2018以降、Rustのモジュールシステムは入門しやすく、かつマスターしにくくなりました。そこで本記事では、その両方を達成することを目指しました。
本記事は2017年に書いた記事 (https://qnighy.hatenablog.com/entry/2017/03/27/070000, https://qnighy.hatenablog.com/entry/2017/04/24/070000 ) と同じテーマで、Rust2018を考慮して改めてまとめ直したものです。
注意: 本記事中のサンプルコードの多くは、Rustの細かい仕様や実装を調べるためのものであり、実際に有用なプログラミングパターンであるとは限りません。また、実質的にバグに近い挙動を説明しているものもあるため、将来にわたって同じ動作であることはあまり保証できません。
Rust2015までのモジュールまわり
ここまではRust2018イディオムを前提に説明をしてきましたが、以降は詳細な説明に入るためにRust2015(特に1.29以前)での状況も軽く説明します。
crate::は存在しませんでした。- extern prelude (外部クレート名がスコープ内に入る機能) は存在しませんでした。
- かわりに、
extern crateで外部クレートのルートをローカルクレートのルート直下にマウントしていました。
- かわりに、
- マクロは名前解決に統合されていませんでした。
- マクロをuseするかわりに、
#[macro_use] extern crateを使っていました。 - マクロをpubでエクスポートするかわりに、
#[macro_export] macro_rules!を使っていました。 - マクロ定義はマクロ使用よりも手前に存在する必要がありました。
- マクロをuseするかわりに、
- 一様パスではありませんでした。
useまたはpub(in)の指定時のみ、暗黙の絶対パスが適用されていました。
また、名前解決自体とは直接は関係ないですが、サブモジュールを別ファイルに分離するときは親ファイルが lib.rs/main.rs/mod.rs である必要がありました。 (たとば、 foo.rs で mod bar; とやって foo/bar.rs を参照するのは禁止されていた。この場合は foo.rs を foo/mod.rs にリネームする必要があった)
なお、1.30以降では、Rust2018へのスムーズな移行のために上記の機能のうち一部をRust2015にも開放しています。
Rust2015-2018移行イディオム
Rustは2015のプロジェクトを2018に移行するための仕組みを提供しています。これは以下の手順を踏みます。
- ビルドする。
cargo fix --editionを実行する。- ビルドする。
Cargo.tomlにedition = "2018"を記載する。- ビルドする。
cargo fix --edition-idiomsを実行する。- ビルドする。
この手順からわかるように、Rust2018は「Rust2015でも2018でもコンパイルが通る中間状態」が(基本的には)存在するように設計されています。モジュールに関しては、以下のような状態がそれにあたります。
extern crateは維持します。::で始まるパスは以下の2種類に分けます。::[外部クレート名]となっているものは維持します。::[クレート内の名前]となっているものはcrate::に置き換えます。
useで使われているパスについても、上記と同様の規則を適用して置き換えます。
以下で述べる仕様の一部は、このような「Rust2015時点での互換性」や「移行状態が存在すること」を達成するために存在しています。
名前解決とは
ここからはあらためて、用語の定義をおさらいします。まずは名前解決について。
たとえば、以下のコード (playground) は42を出力します。
fn main() { dbg!(Vec::new()); } #[allow(non_snake_case)] mod Vec { pub fn new() -> i32 { 42 } }
このように、同じ Vec::new でもそれが何を指すかは場合によって異なります。これを(途中まで)確定させるのが名前解決の役割です。
グローバルな名前だけではなくローカルな名前の解決もここで行われます。たとえば、
fn main() { let x = 42; let x = 53; dbg!(x); }
で、 dbg!(x) 内の x がどちらの let x に対応しているかを決定するのはこのフェーズです。
パス
パスは :: で区切られた名前の列です。ただし、 ::<> という形のもの (turbofish) はジェネリクスパラメーターの一部なので、パスセグメントには数えません。
Vec... 1セグメントのパスstd::vec::Vec... 3セグメントのパスVec<i32>::new... 2セグメントのパスVec::<i32>::new... 2セグメントのパス
特殊なものとしては ::, $crate::, crate::, self::, super:: で始まるものがあります。大雑把には以下のような役割です。
::... Rust2015では絶対パス、Rust2018ではextern preludeパスとして振る舞います。内部的には{{root}}::として扱われます。$crate::,crate::... 絶対パスです。元々は$crateというマクロ専用のパスがあり、同じ用途のcrateがあとから追加されました。$crate::とcrate::の動作はほぼ同じです。self::,super::... 相対パスです。superは先頭またはself::の直後から任意回数続けることができます。
. で始まるもの (s.parse::<i32>() の parse など) はパスではありません。
パスとジェネリクスパラメーター
型文脈と値文脈ではパス要素にジェネリクスパラメーターを付与できます。意味的な扱いは後述しますが、以下のような規則があります。
- モジュール文脈 (
use,pub()) ではジェネリクスパラメーターは利用不可能 - 型文脈 (構文的に型やトレイトが期待されている場所) では
<>でジェネリクスパラメーターを指定可能()->という特殊なジェネリクスパラメーター指定も利用可能。これはFn(i32) -> i32のように使います。::<>も一応使える
- 式文脈 (構文的に式やパターンが期待されている場所) では
::<>でジェネリクスパラメーターを指定可能
型文脈 or 式文脈の区別は難しくないですが、少しわかりづらいケースを書いておきます。
- レコード構造体の初期化構文
MyStruct { .. }は式文脈だが、名前空間(後述)としては型名前空間が用いられる asの直後は型文脈1 as usize < 2のパースに失敗するのはこのため
- 式文脈のパスでも、そのジェネリクスパラメーターは型文脈になる
Vec::<Vec<i32>>::new()のようになる
ジェネリクスパラメーターを実際に処理するのは型推論・型検査時なので、以降はジェネリクスパラメーターには触れません。
修飾子つきパス
<>:: で始まる特殊なパスもあります(これはturbofish ::<> とは別物)。これはさらに以下の2種類に分けられます。
<T>::で始まるもの ...Tは型。<T as Trait>::で始まるもの ...Tは型で、Traitはトレイト。
この2つは名前解決の観点からは大きく異なります。 <T>:: は型相対パスなので、基本的に名前解決ではほとんど何もできずに型推論にその処理を委ねることになります。
一方、 <T as Trait>:: のほうは実際は単なるジェネリクスパラメーターのついたパスです。トレイトにとっての Self は第0ジェネリクスパラメーターであるという原則を念頭に置いて、以下のコードを眺めるとそれがわかると思います。
// `A: Into<B>` は、AをBに変換できるということである。 // AもBも指定しないとき let x: i64 = Into::into(42); // Bだけ指定するとき let x = Into::<i64>::into(42); // Aだけ指定するとき let x: i64 = <i32 as Into<_>>::into(42); // AもBも指定するとき let x = <i32 as Into<i64>>::into(42);
こういった背景から、たとえば <i32 as std::default::Default>::default は4セグメントのパス (= std::default::Default::default) として扱われます。
Def
Rustにおける名前解決はコンパイラのフェーズのひとつで、パス(の途中まで)をDefに解決する処理です。Defは src/librustc/hir/def.rsに定義があります。
Defの内容は多様で、現状は以下の内容が含まれています (読み飛ばしてOKです)
- 型名前空間に属するDef
- モジュール (
mod my_module;) - 構造体 (
struct MyStruct;) - 共用体 (
union MyUnion {}) - 列挙型 (
enum MyEnum {}) - 列挙型のバリアント (
enum MyEnum { MyVariant }) - トレイト (
trait MyTrait {}) - 存在型 (
existential type MyType;) RFC2071 - 型エイリアス (
type MyType = i32;) - 外部型 (
extern { type MyType; }) RFC1861 - トレイトエイリアス (
trait MyTrait = Into<i32>;) RFC1733 - 関連型 (
trait MyTrait { type MyAssocType; }/impl MyTrait for MyStruct { type MyAssocType = i32; }) - 関連存在型 (
impl MyTrait for MyStruct { existential type MyAssocType; }) RFC2071 - プリミティブ型 (
i32) - 型パラメーター (
fn foo<MyTyParam>() {}) - Self型 (
Self) - ツールモジュール (
rustfmtなど)
- モジュール (
- 値名前空間に属するDef
- 関数 (
fn my_function() {}) - 定数 (
const MY_CONST: i32 = 42;) - 定数ジェネリクスパラメーター (
fn foo<const MY_CONST: i32>() {}) RFC2000 - static変数 (
static MY_STATIC: i32 = 42;) - タプル構造体・ユニット構造体のコンストラクタ (
struct MyStruct;/struct MyStruct();) - 列挙型のタプルバリアント・ユニットバリアントのコンストラクタ (
enum MyEnum { MyVariant1, MyVariant2() }) - タプル構造体のSelfコンストラクタ (
Self) - 関連関数 (
trait MyTrait { fn my_assoc_function(); }/impl MyTrait for MyStruct { fn my_assoc_function() {} }) - 関連定数 (
trait MyTrait { const MY_ASSOC_CONST: i32; }/impl MyTrait for MyStruct { const MY_ASSOC_CONST: i32 = 42; })
- 関数 (
- 値名前空間に属するローカルDef
- ローカル束縛 (
let my_var = 42;) - Upvar: キャプチャされた変数 (
let my_upvar = 42; let closure = || { .. my_upvar .. };) - ラベル (
'outer: loop { .. })
- ローカル束縛 (
- マクロ名前空間に属するDef
- マクロ (
macro_rules! my_macro {}) - マクロ以外の属性値 (
#[inline]など)
- マクロ (
このDefの定義にはextern crate と use が含まれていないことに注意してください。これらは既存のDefと同じものを作ります。したがって、以下のコードでは……
type MyType1 = i32; use self::MyType1 as MyType2; type MyType3 = i32;
MyType1 と MyType2 は同じDefですが、 MyType1 と MyType3 は別のDefということになります。
名前解決と型解決の役割分担
名前解決は「パス(の途中まで)をDefに解決する処理」と書いた通り、名前解決の時点ではパスが完全には解決されない場合があります。
たとえば、以下のコード (playground) はエラーになります。これは、 Option が2種類の cloned を持っており、 Option::cloned と書いた時点では確定できないためです。
fn main() { Option::cloned(Some(&42)); //~^ERROR multiple applicable items in scope }
これは、型を明示することで解決できます。 (playground)
fn main() { Option::<&i32>::cloned(Some(&42)); }
この「型を使って解決する」というのは、もっと複雑な形で現れることもあります。たとえば以下のように書いても解決されます。 (playground)
fn main() { Option::<<&Vec<i32> as IntoIterator>::Item>::cloned(Some(&42)); }
こういうものまで名前解決の時点で解決しようとすると複雑になりすぎるため、型に依存して決まる部分は名前解決より後のフェーズ(型推論・型検査)で行われます。そのため、名前解決では以下のようにパスのどこまでが何というDefに解決されたかを最終的な解決結果として用います。
#[derive(Copy, Clone, Debug)] pub struct PathResolution { // 途中までの解決結果 base_def: Def, // 解決できなかったパスセグメントの個数 unresolved_segments: usize, }
たとえば、名前解決は以下のところまで解決します。
let v = Vec::<i32>::new(); // ^^^ ここまで let v = Clone::clone(&v); // ^^^^^^^^^^^^ ここまで let v = Vec::clone(&v); // ^^^ ここまで let v = <Vec<i32> as IntoIterator>::Item::min_value(); // ^^^^^^^^^^^^^^^^^^^ ここまで let it = std::iter::once(42); // ^^^^^^^^^^^^^^^ ここまで
同じ理由で、メソッド名も名前解決では処理されません。
let x = Some(42); let x = x.unwrap(); // ^^^^^^ これがOption::unwrapなのかResult::unwrapなのかは後で決まる
このように、ここで解説している「名前解決」の範疇では型推論に頼らない範囲での解決しか行いません。この2段階解決の仕組みを頭に入れておくと構造の整理がつけやすいと思います。
名前解決の手順
名前解決の責任範囲は明確になりました。その手順を少し細かく割ると以下のようになります。
useとextern crate以外の構造 (モジュール木) を確定させる。useとextern crateを解決する。- これによりモジュールグラフが構成される。
- 各パスの指している内容を(途中まで)確定させる。
- ただし、マクロの名前解決の結果1.に戻る場合がある (RFC1561)
こうして名前解決が終わると、その結果を織り込みながらAST(抽象構文木)をHIR(高レベル中間表現)に変換します。HIRはASTとよく似ていますが名前解決済みなので、後のフェーズは各パスが指すDefを簡単に特定できます。
まとめ
foo::bar::baz などのパスが指す実体(Def)がどこなのかを対応づけるのが名前解決です。ただし、 Vec::new() や x.unwrap() など型に相対して決まるものはこのフェーズでは解決できないので、途中まで解決した状態で進みます。
次回は、モジュールグラフの構造を説明します。