modとファイルシステム

モジュールの例をCargoで新規プロジェクトを生成することから始めるが、バイナリクレートの代わりに、 ライブラリクレートを作成します: 他人が依存として自分のプロジェクトに引き込めるプロジェクトです。 例を挙げると、第2章で議論したrandクレートは、数当てゲームプロジェクトで依存に使用したライブラリクレートです。

何らかの一般的なネットワーク機能を提供するライブラリの骨格を作成します; モジュールと関数の体系化に集中し、 関数の本体にどんなコードが入るかについては気にかけません。このライブラリをcommunicatorと呼びましょう。 ライブラリを生成するために、--binの代わりに--libオプションを渡してください:

$ cargo new communicator --lib
$ cd communicator

Cargoがsrc/main.rsの代わりにsrc/lib.rsを生成したことに注目してください。src/lib.rsには、 以下のような記述があります:

ファイル名: src/lib.rs


# #![allow(unused_variables)]
#fn main() {
#[cfg(test)]
mod tests {
    #[test]
    fn it_works() {
        assert_eq!(2 + 2, 4);
    }
}
#}

Cargoは、--binオプションを使った時に得られる"Hello, world!"バイナリではなく、空のテストを生成して、 ライブラリの事始めをしてくれました。#[]mod testsという記法については、この章の後ほど、 「superを使用して親モジュールにアクセスする」節で見ますが、今のところは、 このコードをsrc/lib.rsの最後に残しておきましょう。

src/main.rsファイルがないので、cargo runコマンドでCargoが実行できるものは何もないわけです。 従って、cargo buildコマンドを使用してライブラリクレートのコードをコンパイルします。

コードの意図によって、いろんなシチュエーションで最適になるライブラリコードを体系化する別のオプションをお目にかけます。

モジュール定義

communicatorネットワークライブラリについて、まずはconnectという関数定義を含むnetworkという名前のモジュールを定義します。 Rustにおいて、モジュール定義は全て、modキーワードから開始します。このコードをsrc/lib.rsファイルの頭、 テストコードの上に追記してください。

ファイル名: src/lib.rs


# #![allow(unused_variables)]
#fn main() {
mod network {
    fn connect() {
    }
}
#}

modキーワードに続いて、モジュール名のnetwork、さらに一連のコードを波かっこ内に記述します。 このブロック内に存在するものは全て、networkという名前空間に属します。今回の場合、 connectという単独の関数があります。この関数をnetworkモジュール外のスクリプトから呼び出したい場合、 モジュールを指定し、以下のように名前空間記法の::を使用する必要があるでしょう: network::connect()

同じsrc/lib.rsファイル内に複数のモジュールを並べることもできます。例として、 connectという関数を含むclientモジュールも用意するには、リスト7-1に示したように追記すればいいわけです。

ファイル名: src/lib.rs


# #![allow(unused_variables)]
#fn main() {
mod network {
    fn connect() {
    }
}

mod client {
    fn connect() {
    }
}
#}

リスト7-1: src/lib.rsに並べて定義されたnetworkモジュールとclientモジュール

これで、network::connect関数とclient::connect関数が用意できました。これらは全く異なる機能を有する可能性があり、 異なるモジュールに存在するので、関数名がお互いに衝突することはありません。

今回の場合、ライブラリを構成しているので、ライブラリビルド時にエントリーポイントとなるファイルは、 src/lib.rsになります。しかし、モジュールを作成するという点に関しては、src/lib.rsには何も特別なことはありません。 ライブラリクレートに対してsrc/lib.rsにモジュールを生成するのと同様に、 バイナリクレートに対してsrc/main.rsにモジュールを生成することもできます。実は、モジュール内にモジュールを書くこともでき、 モジュールが肥大化するにつれて、関連のある機能を一緒くたにし、機能を切り離すのに有用なのです。 コードを体系化すると選択する方法は、コードの部分部分の関連性に対する考え方によります。 例ですが、clientコードとそのconnect関数は、リスト7-2のように、代わりにnetwork名前空間内に存在したら、 ライブラリの使用者にとって意味のあるものになるかもしれません。

ファイル名: src/lib.rs


# #![allow(unused_variables)]
#fn main() {
mod network {
    fn connect() {
    }

    mod client {
        fn connect() {
        }
    }
}
#}

リスト7-2: clientモジュールをnetworkモジュール内に移動させる

src/lib.rsファイル内で、すでにあるmod networkmod clientの定義をリスト7-2のものと置き換えると、 clientモジュールはnetworkの内部モジュールになるわけです。関数、 network::connectnetwork::client::connectはどちらもconnectという名前ですが、 異なる名前空間にあるので、互いに干渉することはありません。

このように、モジュールは階層構造を形成します。src/lib.rsの中身が頂点に立ち、サブモジュールが子供になるわけです。 リスト7-1の例を階層構造という観点で見たときの構造は、以下のような感じになります:

communicator
 ├── network
 └── client

さらに、リスト7-2の例に対応する階層構造は、以下の通りです:

communicator
 └── network
     └── client

この階層構造は、リスト7-2において、clientモジュールはnetworkモジュールの兄弟というよりも、子供になっていることを示しています。 より複雑なプロジェクトなら、たくさんのモジュールが存在し、把握するのに論理的に体系化しておく必要があるでしょう。 プロジェクト内で「論理的」とは、あなた次第であり、ライブラリ作成者と使用者がプロジェクトの領域についてどう考えるか次第でもあるわけです。 こちらで示したテクニックを使用して、並列したモジュールや、ネストしたモジュールなど、どんな構造のモジュールでも、 作成してください。

モジュールを別ファイルに移す

モジュールは階層構造をなす……コンピュータにおいて、もっと見慣れた構造に似ていませんか: そう、ファイルシステムです! Rustのモジュールシステムを複数のファイルで使用して、プロジェクトを分割するので、 全部がsrc/lib.rssrc/main.rsに存在することにはならなくなります。これの例として、 リスト7-3のようなコードから始めましょう。

ファイル名: src/lib.rs


# #![allow(unused_variables)]
#fn main() {
mod client {
    fn connect() {
    }
}

mod network {
    fn connect() {
    }

    mod server {
        fn connect() {
        }
    }
}
#}

リスト7-3: 全てsrc/lib.rsに定義された三つのモジュール、 clientnetworknetwork::server

src/lib.rsファイルのモジュール階層は、こうなっています:

communicator
 ├── client
 └── network
     └── server

これらのモジュールが多数の関数を含み、その関数が長ったらしくなってきたら、このファイルをスクロールして、 弄りたいコードを探すのが困難になるでしょう。関数が一つ以上のmodブロックにネストされているので、 関数の中身となるコードも長ったらしくなってしまうのです。これだけで、clientnetworkserverモジュールをsrc/lib.rsから分け、 単独のファイルに配置するには十分でしょう。

最初に、clientモジュールのコードをclientモジュールの宣言だけに置き換えましょう。 すると、src/lib.rsはリスト7-4のコードのようになります。

ファイル名: src/lib.rs

mod client;

mod network {
    fn connect() {
    }

    mod server {
        fn connect() {
        }
    }
}

リスト7-4: clientモジュールの中身を抽出するが、宣言はsrc/lib.rsに残したまま

一応、clientモジュールをここで宣言していますが、ブロックをセミコロンで置換したことで、 clientモジュールのスコープのコードは別の場所を探すようにコンパイラに指示しているわけです。 言い換えると、mod client;の行は、以下のような意味になります:

mod client {
    // contents of client.rs
}

さて、このモジュール名の外部ファイルを作成する必要が出てきました。src/ ディレクトリ内にclient.rsファイルを作成し、 開いてください。それから以下のように入力してください。前段階で削除したclientモジュールのconnect関数です:

ファイル名: src/client.rs


# #![allow(unused_variables)]
#fn main() {
fn connect() {
}
#}

このファイルには、mod宣言が必要ないことに着目してください。なぜなら、src/lib.rsmodを使って、 もうclientモジュールを宣言しているからです。このファイルは、clientモジュールの中身を提供するだけなのです。 ここにもmod clientを記述したら、clientclientという名前のサブモジュールを与えることになってしまいます!

コンパイラは、標準でsrc/lib.rsだけを検索します。プロジェクトにもっとファイルを追加したかったら、 src/lib.rsで他のファイルも検索するよう、コンパイラに指示する必要があるのです; このため、mod clientsrc/lib.rsに定義し、 src/client.rsには定義できなかったのです。

これでプロジェクトは問題なくコンパイルできるはずです。まあ、警告がいくつか出るんですが。 cargo runではなく、cargo buildを使うことを忘れないでください。バイナリクレートではなく、 ライブラリクレートだからですね:

$ cargo build
   Compiling communicator v0.1.0 (file:///projects/communicator)

warning: function is never used: `connect`
(警告: 関数は使用されていません: `connect`)
 --> src/client.rs:1:1
  |
1 | / fn connect() {
2 | | }
  | |_^
  |
  = note: #[warn(dead_code)] on by default

warning: function is never used: `connect`
 --> src/lib.rs:4:5
  |
4 | /     fn connect() {
5 | |     }
  | |_____^

warning: function is never used: `connect`
 --> src/lib.rs:8:9
  |
8 | /         fn connect() {
9 | |         }
  | |_________^

これらの警告は、全く使用されていない関数があると忠告してくれています。今は、警告を危惧する必要はありません; この章の後ほど、「pubで公開するか制御する」節で特定します。嬉しいことにただの警告です; プロジェクトはビルドに成功しました!

次に、同様のパターンでnetworkモジュールも単独のファイルに抽出しましょう。 src/lib.rsで、networkモジュールの本体を削除し、宣言にセミコロンを付加してください。こんな感じです:

ファイル名: src/lib.rs

mod client;

mod network;

それから新しいsrc/network.rsファイルを作成して、以下のように入力してください:

ファイル名: src/network.rs


# #![allow(unused_variables)]
#fn main() {
fn connect() {
}

mod server {
    fn connect() {
    }
}
#}

このモジュールファイル内にもまだmod宣言があることに気付いてください; serverはまだnetworkのサブモジュールにしたいからです。

再度cargo buildしてください。成功!抽出すべきモジュールがもう1個あります: serverです。 これはサブモジュール(つまり、モジュール内のモジュール)なので、 モジュール名に倣ったファイルにモジュールを抽出するという今の手法は、通用しません。いずれにしても、 エラーが確認できるように、試してみましょう。まず、src/network.rsファイルをserverモジュールの中身を含む代わりに、 mod server;となるように変更してください。

ファイル名: src/network.rs

fn connect() {
}

mod server;

そして、src/server.rsファイルを作成し、抽出したserverモジュールの中身を入力してください:

ファイル名: src/server.rs


# #![allow(unused_variables)]
#fn main() {
fn connect() {
}
#}

cargo buildを実行しようとすると、リスト7-5に示したようなエラーが出ます:

$ cargo build
   Compiling communicator v0.1.0 (file:///projects/communicator)
error: cannot declare a new module at this location
(エラー: この箇所では新規モジュールを宣言できません)
 --> src/network.rs:4:5
  |
4 | mod server;
  |     ^^^^^^
  |
note: maybe move this module `src/network.rs` to its own directory via `src/network/mod.rs`
(注釈: もしかして、`src/network.rs`というこのモジュールを`src/network/mod.rs`経由で独自のディレクトリに移すの)
 --> src/network.rs:4:5
  |
4 | mod server;
  |     ^^^^^^
note: ... or maybe `use` the module `server` instead of possibly redeclaring it
(注釈: それとも、再度宣言する可能性はなく、`server`というモジュールを`use`したの)
 --> src/network.rs:4:5
  |
4 | mod server;
  |     ^^^^^^

リスト7-5: serverサブモジュールをsrc/server.rsに抽出しようとしたときのエラー

エラーは、この箇所では新規モジュールを宣言できませんと忠告し、src/network.rsmod server;行を指し示しています。 故に、src/network.rsは、src/lib.rsと何かしら違うのです: 理由を知るために読み進めましょう。

リスト7-5の真ん中の注釈は、非常に有用です。というのも、まだ話題にしていないことを指摘しているからです。

note: maybe move this module `network` to its own directory via
`network/mod.rs`

以前行ったファイル命名パターンに従い続けるのではなく、注釈が提言していることをすることができます:

  1. 親モジュール名であるnetworkという名前の新規ディレクトリを作成する。
  2. src/network.rsファイルをnetworkディレクトリに移し、 src/network/mod.rsと名前を変える。
  3. サブモジュールファイルのsrc/server.rsnetworkディレクトリに移す。

以下が、これを実行するコマンドです:

$ mkdir src/network
$ mv src/network.rs src/network/mod.rs
$ mv src/server.rs src/network

cargo buildを走らせたら、コンパイルは通ります(まだ警告はありますけどね)。 それでも、モジュールの配置は、リスト7-3でsrc/lib.rsに全てのコードを収めていたときと全く同じになります:

communicator
 ├── client
 └── network
     └── server

対応するファイルの配置は、以下のようになっています:

└── src
    ├── client.rs
    ├── lib.rs
    └── network
        ├── mod.rs
        └── server.rs

では、network::serverモジュールを抽出したかったときに、 なぜ、src/network.rsファイルをsrc/network/mod.rsファイルに変更し、 network::serverのコードをnetworkディレクトリ内のsrc/network/server.rsに置かなければならなかったのでしょうか? なぜ、単にnetwork::serverモジュールをsrc/server.rsに抽出できなかったのでしょうか? 理由は、server.rsファイルがsrcディレクトリにあると、コンパイラが、 servernetworkのサブモジュールと考えられることを検知できないからです。 ここでのコンパイラの動作をはっきりさせるために、以下のようなモジュール階層をもつ別の例を考えましょう。 こちらでは、定義は全てsrc/lib.rsに存在します。

communicator
 ├── client
 └── network
     └── client

この例でも、モジュールは3つあります: clientnetworknetwork::clientです。 以前と同じ手順を経てモジュールをファイルに抽出すると、clientモジュール用にsrc/client.rsを作成することになるでしょう。 networkモジュールに関しては、src/network.rsを作成します。しかし、 network::clientモジュールをsrc/client.rsファイルに抽出することはできません。 もうトップ階層のclientモジュールとして存在するからです! clientnetwork::client双方のコードをsrc/client.rsファイルに書くことができたら、 コンパイラは、コードがclient用なのか、network::client用なのか知る術を失ってしまいます。

従って、networkモジュールのnetwork::clientサブモジュールをファイルに抽出するには、 src/network.rsファイルではなく、networkモジュールのディレクトリを作成する必要があったわけです。 そうすれば、networkモジュールのコードは、src/network/mod.rsファイルに移ることになり、 network::clientというサブモジュールは専用のsrc/network/client.rsファイルを持てるわけです。 これで、頂点にあるsrc/client.rsは間違いなく、clientモジュールに属するコードになるわけです。

モジュールファイルシステムの規則

ファイルに関するモジュール規則をまとめましょう:

  • fooという名前のモジュールにサブモジュールがなければ、fooの定義は、 foo.rsというファイルに書くべきです。
  • fooというモジュールに本当にサブモジュールがあったら、fooの定義は、 foo/mod.rsというファイルに書くべきです。

これらのルールは再帰的に適用されるので、fooというモジュールにbarというサブモジュールがあり、 barにはサブモジュールがなければ、srcディレクトリには以下のようなファイルが存在するはずです:

├── foo
│   ├── bar.rs (`foo::bar`内の定義を含む)
│   └── mod.rs (`mod bar`を含む、`foo`内の定義を含む)

モジュールは、親モジュールのファイル内でmodキーワードを使って宣言されるべきなのです。

次は、pubキーワードについて話し、警告を取り除きます!