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() { } } #}
これで、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() { } } } #}
src/lib.rsファイル内で、すでにあるmod network
とmod client
の定義をリスト7-2のものと置き換えると、
client
モジュールはnetwork
の内部モジュールになるわけです。関数、
network::connect
とnetwork::client::connect
はどちらもconnect
という名前ですが、
異なる名前空間にあるので、互いに干渉することはありません。
このように、モジュールは階層構造を形成します。src/lib.rsの中身が頂点に立ち、サブモジュールが子供になるわけです。 リスト7-1の例を階層構造という観点で見たときの構造は、以下のような感じになります:
communicator
├── network
└── client
さらに、リスト7-2の例に対応する階層構造は、以下の通りです:
communicator
└── network
└── client
この階層構造は、リスト7-2において、client
モジュールはnetwork
モジュールの兄弟というよりも、子供になっていることを示しています。
より複雑なプロジェクトなら、たくさんのモジュールが存在し、把握するのに論理的に体系化しておく必要があるでしょう。
プロジェクト内で「論理的」とは、あなた次第であり、ライブラリ作成者と使用者がプロジェクトの領域についてどう考えるか次第でもあるわけです。
こちらで示したテクニックを使用して、並列したモジュールや、ネストしたモジュールなど、どんな構造のモジュールでも、
作成してください。
モジュールを別ファイルに移す
モジュールは階層構造をなす……コンピュータにおいて、もっと見慣れた構造に似ていませんか: そう、ファイルシステムです! Rustのモジュールシステムを複数のファイルで使用して、プロジェクトを分割するので、 全部がsrc/lib.rsやsrc/main.rsに存在することにはならなくなります。これの例として、 リスト7-3のようなコードから始めましょう。
ファイル名: src/lib.rs
# #![allow(unused_variables)] #fn main() { mod client { fn connect() { } } mod network { fn connect() { } mod server { fn connect() { } } } #}
src/lib.rsファイルのモジュール階層は、こうなっています:
communicator
├── client
└── network
└── server
これらのモジュールが多数の関数を含み、その関数が長ったらしくなってきたら、このファイルをスクロールして、
弄りたいコードを探すのが困難になるでしょう。関数が一つ以上のmodブロックにネストされているので、
関数の中身となるコードも長ったらしくなってしまうのです。これだけで、client
、network
、server
モジュールをsrc/lib.rsから分け、
単独のファイルに配置するには十分でしょう。
最初に、client
モジュールのコードをclient
モジュールの宣言だけに置き換えましょう。
すると、src/lib.rsはリスト7-4のコードのようになります。
ファイル名: src/lib.rs
mod client;
mod network {
fn connect() {
}
mod server {
fn connect() {
}
}
}
一応、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.rsにmod
を使って、
もうclient
モジュールを宣言しているからです。このファイルは、client
モジュールの中身を提供するだけなのです。
ここにもmod client
を記述したら、client
にclient
という名前のサブモジュールを与えることになってしまいます!
コンパイラは、標準でsrc/lib.rsだけを検索します。プロジェクトにもっとファイルを追加したかったら、
src/lib.rsで他のファイルも検索するよう、コンパイラに指示する必要があるのです; このため、mod client
をsrc/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;
| ^^^^^^
エラーは、この箇所では新規モジュールを宣言できません
と忠告し、src/network.rsのmod server;
行を指し示しています。
故に、src/network.rsは、src/lib.rsと何かしら違うのです: 理由を知るために読み進めましょう。
リスト7-5の真ん中の注釈は、非常に有用です。というのも、まだ話題にしていないことを指摘しているからです。
note: maybe move this module `network` to its own directory via
`network/mod.rs`
以前行ったファイル命名パターンに従い続けるのではなく、注釈が提言していることをすることができます:
- 親モジュール名であるnetworkという名前の新規ディレクトリを作成する。
- src/network.rsファイルをnetworkディレクトリに移し、 src/network/mod.rsと名前を変える。
- サブモジュールファイルのsrc/server.rsをnetworkディレクトリに移す。
以下が、これを実行するコマンドです:
$ 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ディレクトリにあると、コンパイラが、
server
はnetwork
のサブモジュールと考えられることを検知できないからです。
ここでのコンパイラの動作をはっきりさせるために、以下のようなモジュール階層をもつ別の例を考えましょう。
こちらでは、定義は全てsrc/lib.rsに存在します。
communicator
├── client
└── network
└── client
この例でも、モジュールは3つあります: client
、network
、network::client
です。
以前と同じ手順を経てモジュールをファイルに抽出すると、client
モジュール用にsrc/client.rsを作成することになるでしょう。
network
モジュールに関しては、src/network.rsを作成します。しかし、
network::client
モジュールをsrc/client.rsファイルに抽出することはできません。
もうトップ階層のclient
モジュールとして存在するからです!
client
とnetwork::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
キーワードについて話し、警告を取り除きます!