Rustのモジュールを詳細に理解する(3) モジュールグラフの構造

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


モジュール木からモジュールグラフへ

Rustのモジュールはファイルシステムのように木構造がベースになっています。(前述のように、mod m1; のようになっているものは構文解析の段階で展開されているので、ここでは統一して扱います)

pub mod m1 {
    pub mod m2 {
        mod m3 {
            
        }
        mod m4 {
            
        }
    }
}

しかし、 use が絡むと話が難しくなります。これはシンボリックリンクのようなもので、相互参照や再帰的なモジュールを作ることができてしまいます。

pub mod m1 {
    pub mod m2 {
        mod m3 {
            // m3とm4で相互にインポートしている
            use super::m4;
        }
        mod m4 {
            use super::m3;
        }
        // m1::m2::m1::m2::...
        pub use crate::m1;
    }
}

このモジュールグラフを構築する部分が名前解決の要ですが、glob importなどがあるため単純にはいきません。また、木構造といっても単純な木構造とは言えない事情がいくつかあります。これらの詳細な事情をひとつずつ解説していきます。

名前空間

Rustではひとつのモジュールに同じ名前のDefを最大3個まで紐付けられます。これを名前空間 (namespace)といい、以下の3つの名前空間があります。

(C++C#では、Rustのモジュールに対応するもののことを名前空間と呼んでいます。Rustの名前空間C言語の名前空間などに近い概念だといえます。)

たとえば、以下のようにタプル構造体を定義した場合は実際には2つのDefが紐付いています。 (ユニット構造体の場合も同様)

struct Color(u8, u8, u8);
fn main() {
    let color: Color = Color(255, 255, 255);
    //                 ^^^^^ 関数としてのColor (値名前空間)
    //         ^^^^^ 型としてのColor (型名前空間)
}

レコード構造体の場合は型名前空間しか消費しないので、関数とのオーバーロードが可能です。(推奨するわけではありません)

struct Color {
    red: u8,
    green: u8,
    blue: u8,
}

#[allow(non_snake_case)]
fn Color(red: u8, green: u8, blue: u8) -> Color {
    Color { red, green, blue }
}

RFC0234の規定により、列挙型のレコードバリアントは値名前空間も予約することになっています。

また、 serde::Serialize はトレイトとしてもderiveマクロとしても振る舞います。

use serde::Serialize;

// トレイトとしてのserde::Serializeを参照する場合
impl Serialize for MyStruct1 { .. }

// deriveマクロとしてのserde::Serializeを参照する場合

#[derive(Serialize)]
struct MyStruct2 { .. }

use as _

Rust1.33.0から、以下のような別名インポートが可能になりました。(RFC2166)

// Read/Writeをスコープ内トレイトの解決に利用する。
// しかし、このモジュールのRead/Writeという名前を汚染しない。
use std::io::{Read as _, Write as _};

これは後述するスコープ内トレイトの解決のために使われますが、特徴として as _ で作られた名前は重複しないという特殊仕様があります。これは名前解決前に _ を実際には別々のsymbol idに割り当てることで実装されています。Rustコンパイラ識別子を整数に変換して保持しているので、 _ に重複して別々の整数を割り当てるというハックが可能というわけです。

なお、意図してかは不明ですが、as _ でインポートされたものもglob importに含まれるようです。 (playground)

mod m1 {
    pub use std::io::BufRead as _;
}
use m1::*;

fn main() {
    std::io::stdin().lock().read_line(&mut String::new()).unwrap();
}

無名のconst

コンパイルアサーションのための const _ (RFC2526; 不安定機能) でも上記と同様の仕組みをとっています。 (playground)

#![feature(underscore_const_names)]

const _: i32 = 42;
const _: i32 = 53;

これらはトレイトではないので、外から見て意味のある挙動はないはずです。 (名指しできないのにスコープ内に置いて意味があるのはトレイトだけ)

クレートルート

先ほど書いたようにモジュール構造は(extern crateuse を除いて考えると)木構造になっています。

mod m1 {}
mod m2 {}

したがって、上の例ではまずクレートのルート(根)が暗黙の最上位モジュールとして存在し、その直下に m1m2 があるという構造になります。

そして、依存している他のクレートはそれぞれが別のモジュール木になっています。つまりモジュール木というのは不正確でモジュール森と考えたほうが正確です。他のクレートを参照できる仕組みについては後述します。

// serdeクレートのルートを crate::serde にマウントする (Rust2015時代のイディオム)
extern crate serde;

// failureクレートのルートを crate::failure にマウントせず組み込みプレリュードとして直接呼び出す (Rust2018時代のイディオム)
use failure::Fail;

非正規モジュール

クレートルートと mod 以外に、モジュールとして振る舞うものが3つありますenumtraitブロックです。

enum MyEnum {
    // MyEnumモジュールの直下にVariant1, Variant2があるものとして扱われる
    Variant1,
    Variant2,
}

trait MyTrait {
    // MyTraitモジュールの直下にmy_methodがあるものとして扱われる
    fn my_method(&self);
}

fn my_function() {
    // my_functionモジュールの直下に匿名モジュール[0]があるものとして扱われる
    // 匿名モジュール[0]の直下にはAと匿名モジュール[1]がある
    struct A;
    {
        // 匿名モジュール[1]の直下にはBがある
        struct B;
    }
}

enumがモジュールとして扱われているのはもちろん、以下を可能にするためです。

enum MyEnum {
    // デフォルトでは MyEnum::Variant1, MyEnum::Variant2 として参照可能
    Variant1,
    Variant2,
}

// こうすることで単に Variant1, Variant2 として参照できるようになる
use self::MyEnum::*;

traitがモジュールなのは名前解決時にメソッド名まで解決してしまいたいからです。 (型相対なパスは型推論をしないと解決できないが、トレイト相対なパスは型推論によらず解決できる)

let x: i32 = Default::default();
//           ^^^^^^^^^^^^^^^^ ここまで解決する
let x = <i32 as Default>::default();
//              ^^^^^^^^^^^^^^^^^ ここまで解決する
let x = i32::default();
//      ^^^ ここまでしか解決しない (Defaultトレイトのdefaultかどうかははっきりしないので)

(ただし、トレイトアイテムのuseは別途禁止されています)

ブロックひとつひとつがモジュールであるおかげで、ローカルでしか使わない構造体を定義したり、一時的なインポートをすることができます。

fn do_something_with_io() {
    // この関数のみで有効なuse
    use std::fmt;
    {
        // このブロック内でしか使わない型
        struct StructForInternalUse;
        impl fmt::Debug for StructForInternalUse { ... }
        ...
    }
}

非正規モジュールは相対パスのときに無視されます。 (self:: は最も近い正規モジュールを指す)

const X: i32 = 42;

fn main() {
    const X: i32 = 53;
    // スコープ内には2つのXがあるので、より内側にある53が採用される
    assert_eq!(X, 53);
    // 最も近い正規モジュールはクレートルートなので、その直下のXである42が採用される
    assert_eq!(self::X, 42);
}

mod, enum, trait として定義されているものはDefとして親モジュールから名前で参照できますが、ブロックに対応する匿名モジュールは親モジュールからのリンクはなく、子から親へのリンクしかありません。

エイリアス列挙型のバリアント

以下のコードは現在のRustでは動きません。 (playground)

// #![feature(type_alias_enum_variants)]

enum MyEnum {
    Variant1,
    Variant2,
}

// 同じDef
use MyEnum as MyEnum2;

// 異なるDef
type MyEnum3 = MyEnum;

fn main() {
    // OK
    let x1 = MyEnum::Variant1;

    // OK
    let x2 = MyEnum2::Variant1;
    
    // エラー。 #![feature(type_alias_enum_variants)] が必要
    let x3 = MyEnum3::Variant1;
}

前述のように、列挙型のバリアントに出てくる ::use MyEnum::*; などを可能にするために名前解決によって解決されます。ところが、名前解決の観点からは型エイリアスは同じDefではないので、列挙型に(モジュール的に)ぶら下がっているバリアントは引き継ぎません。

RFC2338 でこの不便が解消される予定です。RFC2338の現状の実装では名前解決に手を入れていません。かわりに、型解決の段階で関連アイテムの亜種として処理しています。本記事の手前のほうで、「名前解決はパスを途中まで解決する」と書きましたが、それで言うと以下のようになります。

#![feature(type_alias_enum_variants)]

enum MyEnum {
    Variant1,
    Variant2,
}
use MyEnum as MyEnum2;
type MyEnum3 = MyEnum;

fn main() {
    let x1 = MyEnum::Variant1;
    //       ^^^^^^^^^^^^^^^^ 名前解決でここまで解決される

    let x2 = MyEnum2::Variant1;
    //       ^^^^^^^^^^^^^^^^^ 名前解決でここまで解決される
    
    let x3 = MyEnum3::Variant1;
    //       ^^^^^^^ 名前解決でここまで解決される
    //                ^^^^^^^^ 型推論時にここを解決する
}

この実装ではRFC本文に書かれている例(type alias enum variant を use しようとしている)がコンパイルできません。これが意図したものであるかどうかはやや不明で、eddyb氏の発言を見るにはじめからあまり考慮していないようにも見えます。目的論的にも、 use が必要な場面ではもとの列挙型から引っ張ってくれば十分とも言えるので、RFCの本文がoutdatedであると考える余地があります。

もし type alias enum variant を use でも使えるようにするとなった場合は、モジュール木の構造自体に何らかの影響を及ぼすと思われます。

トレイト別名

RFC1733のトレイト別名はトレイト別名から関連アイテム(メソッド・関連関数・関連定数・関連型)を引くことについて言及していないので、今のところ名前解決への影響はなさそうです。

まとめ

モジュール木はナイーブにはモジュールをノードとし、識別子をラベルとするラベルつき木といえますが、匿名モジュールや名前空間などがあるため正確にはより複雑な構造を持っていることを説明しました。

次回use の挙動と、それを解決するためのアルゴリズムについて説明します。