https://zenn.dev/msakuta/articles/83f9991b2aba62

Rust のモジュールシステムは私の知る中でもトップクラスによくできた仕組みだと思います。特にリファクタリングによってモジュールを再構成するときのやりやすさは他の言語では経験できないものです。例えばそれなりの規模の Python プロジェクトを回帰バグを導入せずにモジュール構造のリファクタリングするのは不可能に近いですが、 Rust ではそのような不安を覚えたためしがありません。

Rust のモジュールシステムがどういうものかは、 The book にも書かれていますし、すでに大量のガイドが書かれていると思います。しかし、どのように使うべきかについては意外なほど情報が少なく感じます。

ベストプラクティスというのもおこがましいですが、数年使ってきて Rust のモジュールシステムを使う上でスムーズに感じる方法をまとめておきたいと思います。

Rust のモジュールシステム

本稿の主題はモジュールシステムが何なのかではなく、どのように使うべきかということですが、少しおさらいしておこうと思います。

Rust の翻訳単位 (compilation unit) は crate です。翻訳単位とは、 C や C++ でいうソースファイルのことで、ソース(テキスト)から中間ファイルへ翻訳する最小単位です。 Rust はこの翻訳単位を crate という大きなくくりにしており、その中をモジュールで構造化できるようにしてあります[1]

実行可能形式の出力なら src/main.rs、ライブラリなら src/lib.rs が crate の入り口になります。

そして特に Rust に特徴的だといえるのが、モジュールとファイル構造が一致しているとは限らないというところです。これは C++ の名前空間に少し似ていますが、そこまで自由に構成できるわけではなく、ファイルに分けるとしたらモジュール構造に合わせる必要があります。

例えば、一つのソースファイルに以下のように sub サブモジュールを含めることができます。このとき、サブモジュール内のアイテムは pub, pub(super), pub(crate) のいずれかの公開宣言がされている必要があります。

main.rs

fn main() {
  sub::greet();
}

mod sub {
  pub fn greet() {
    println!("Hello");
  }
}

同じことは sub.rs というファイルにサブモジュールの内容を移しても実現できます。このとき、元となるソースファイルに mod sub; という宣言を含めておく必要があります。これによってコンパイラは sub.rs というファイルのモジュール sub の内容があることを認識します。

main.rs

mod sub;

fn main() {
  sub::greet();
}

pub fn greet() {
  println!("Hello");
}

さらに孫モジュールを定義することもできます。

pub fn greet() {
  println!(subsub::GREET_TEXT);
}

mod subsub {
  pub const GREET_TEXT: &str = "Hello";
}

さて、孫モジュールをファイルに分けるときの方法が少し特殊です。