Rustのモジュールを詳細に理解する(2) 名前解決の役割と用語

概要: 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の細かい仕様や実装を調べるためのものであり、実際に有用なプログラミングパターンであるとは限りません。また、実質的にバグに近い挙動を説明しているものもあるため、将来にわたって同じ動作であることはあまり保証できません。


Rust2015までのモジュールまわり

ここまではRust2018イディオムを前提に説明をしてきましたが、以降は詳細な説明に入るためにRust2015(特に1.29以前)での状況も軽く説明します。

  • crate:: は存在しませんでした。
    • かわりに、 :: で始まる旧式の絶対パスを使っていました。
    • また、マクロ衛生性のために $crate:: という特殊な絶対パスがありました。
  • extern prelude (外部クレート名がスコープ内に入る機能) は存在しませんでした。
    • かわりに、 extern crate で外部クレートのルートをローカルクレートのルート直下にマウントしていました。
  • マクロは名前解決に統合されていませんでした。
    • マクロをuseするかわりに、 #[macro_use] extern crate を使っていました。
    • マクロをpubでエクスポートするかわりに、 #[macro_export] macro_rules! を使っていました。
    • マクロ定義はマクロ使用よりも手前に存在する必要がありました。
  • 一様パスではありませんでした。
    • use または pub(in) の指定時のみ、暗黙の絶対パスが適用されていました。

また、名前解決自体とは直接は関係ないですが、サブモジュールを別ファイルに分離するときは親ファイルが lib.rs/main.rs/mod.rs である必要がありました。 (たとば、 foo.rsmod bar; とやって foo/bar.rs を参照するのは禁止されていた。この場合は foo.rsfoo/mod.rs にリネームする必要があった)

なお、1.30以降では、Rust2018へのスムーズな移行のために上記の機能のうち一部をRust2015にも開放しています。

Rust2015-2018移行イディオム

Rustは2015のプロジェクトを2018に移行するための仕組みを提供しています。これは以下の手順を踏みます。

  1. ビルドする。
  2. cargo fix --edition を実行する。
  3. ビルドする。
  4. Cargo.tomledition = "2018" を記載する。
  5. ビルドする。
  6. cargo fix --edition-idioms を実行する。
  7. ビルドする。

この手順からわかるように、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 crateuse が含まれていないことに注意してください。これらは既存のDefと同じものを作ります。したがって、以下のコードでは……

type MyType1 = i32;
use self::MyType1 as MyType2;
type MyType3 = i32;

MyType1MyType2 は同じDefですが、 MyType1MyType3 は別の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段階解決の仕組みを頭に入れておくと構造の整理がつけやすいと思います。

名前解決の手順

名前解決の責任範囲は明確になりました。その手順を少し細かく割ると以下のようになります。

  1. useextern crate 以外の構造 (モジュール木) を確定させる。
  2. useextern crate を解決する。
    • これによりモジュールグラフが構成される。
  3. 各パスの指している内容を(途中まで)確定させる。
    • ただし、マクロの名前解決の結果1.に戻る場合がある (RFC1561)

こうして名前解決が終わると、その結果を織り込みながらAST(抽象構文木)をHIR(高レベル中間表現)に変換します。HIRはASTとよく似ていますが名前解決済みなので、後のフェーズは各パスが指すDefを簡単に特定できます。

まとめ

foo::bar::baz などのパスが指す実体(Def)がどこなのかを対応づけるのが名前解決です。ただし、 Vec::new()x.unwrap() など型に相対して決まるものはこのフェーズでは解決できないので、途中まで解決した状態で進みます。

次回は、モジュールグラフの構造を説明します。