肥大化していくプロジェクトをパッケージ、クレート、モジュールを利用して管理する
大きなプログラムを書く時、そのすべてを頭の中に入れておくのは不可能になるため、コードのまとまりを良くすることが重要になります。 関係した機能をまとめ、異なる特徴を持つコードを分割することにより、特定の機能を実装しているコードを見つけたり、機能を変更したりするためにどこを探せば良いのかを明確にできます。
私達がこれまでに書いてきたプログラムは、一つのファイル内の一つのモジュール内にありました。 プロジェクトが大きくなるにつれて、これを複数のモジュールに、ついで複数のファイルに分割することで、プログラムを整理することができます。 パッケージは複数のバイナリクレートからなり、またライブラリクレートを1つもつこともできます。 パッケージが大きくなるにつれて、その一部を抜き出して分離したクレートにし、外部依存とするのもよいでしょう。 この章ではそれらのテクニックすべてを学びます。 相互に関係し合い、同時に成長するパッケージの集まりからなる巨大なプロジェクトには、 Cargoがワークスペースという機能を提供します。これは14章のCargoワークスペースで解説します。
機能をグループにまとめられることに加え、実装の詳細がカプセル化されることにより、コードをより高いレベルで再利用できるようになります: 手続きを実装し終えてしまえば、他のコードはそのコードの公開されたインターフェースを通じて、実装の詳細を知ることなくそのコードを呼び出すことができるのです。 コードをどう書くかによって、どの部分が他のコードにも使える公開のものになるのか、それとも自分だけが変更できる非公開のものになるのかが決定されます。 これもまた、記憶しておくべき細部を制限してくれる方法のひとつです。
関係する概念にスコープがあります: コードが記述されているネストされた文脈には、「スコープ内」として定義される名前の集合があります。 コードを読んだり書いたりコンパイルしたりする時には、プログラマーやコンパイラは特定の場所にある特定の名前が、変数・関数・構造体・enum・モジュール・定数・その他のどの要素を表すのか、そしてその要素は何を意味するのかを知る必要があります。 そこでスコープを作り、どの名前がスコープ内/スコープ外にあるのかを変更することができます。 同じ名前のものを2つ同じスコープ内に持つことはできません。そこで、名前の衝突を解決するための方法があります。
Rustには、どの詳細を公開するか、どの詳細を非公開にするか、どの名前がプログラムのそれぞれのスコープにあるか、といったコードのまとまりを保つためのたくさんの機能があります。 これらの機能は、まとめて「モジュールシステム」と呼ばれることがあり、以下のようなものが含まれます。
- パッケージ: クレートをビルドし、テストし、共有することができるCargoの機能
- クレート: ライブラリか実行可能ファイルを生成する、木構造をしたモジュール群
- モジュール と use: これを使うことで、パスの構成、スコープ、公開するか否かを決定できます
- パス: 要素(例えば構造体や関数やモジュール)に名前をつける方法
この章では、これらの機能をすべて学び、これらがどう相互作用するかについて議論し、これらをどう使ってスコープを制御するのかについて説明します。 この章を読み終わる頃には、モジュールシステムをしっかりと理解し、熟練者のごとくスコープを扱うことができるようになっているでしょう!
最初に学ぶモジュールシステムの要素は、パッケージとクレートです。 クレートはバイナリかライブラリのどちらかです。 クレートルート (crate root) とは、Rustコンパイラの開始点となり、クレートのルートモジュールを作るソースファイルのことです(モジュールについて詳しくは「モジュールを定義して、スコープとプライバシーを制御する」のセクションで説明します)。 パッケージ はある機能群を提供する1つ以上のクレートです。 パッケージは Cargo.toml という、それらのクレートをどのようにビルドするかを説明するファイルを持っています。
パッケージが何を持ってよいかはいくつかのルールで決まっています。 パッケージは0個か1個のライブラリクレートを持っていないといけません。それ以上は駄目です。 バイナリクレートはいくらでも持って良いですが、少なくとも(ライブラリでもバイナリでも良いですが)1つのクレートを持っていないといけません。
パッケージを作る時に何が起こるか見てみましょう。 まず、cargo new
というコマンドを入力します:
$ cargo new my-project
Created binary (application) `my-project` package
$ ls my-project
Cargo.toml
src
$ ls my-project/src
main.rs
このコマンドを入力したとき、Cargoは Cargo.toml ファイルを作り、パッケージを作ってくれました。 Cargo.toml の中身を見ても、src/main.rs については何も書いてありません。これは、Cargoは src/main.rs が、パッケージと同じ名前を持つバイナリクレートのクレートルートであるという慣習に従っているためです。 同じように、Cargoはパッケージディレクトリに src/lib.rs が含まれていたら、パッケージにはパッケージと同じ名前のライブラリクレートが含まれており、src/lib.rs がそのクレートルートなのだと判断します。 Cargoはクレートルートファイルを rustc
に渡し、ライブラリやバイナリをビルドします。
今、このパッケージには src/main.rs しか含まれておらず、つまりこのパッケージはmy-project
という名前のバイナリクレートのみを持っているということです。 もしパッケージが src/main.rs と src/lib.rs を持っていたら、クレートは2つになります:どちらもパッケージと同じ名前を持つ、ライブラリクレートとバイナリクレートです。 ファイルを src/bin ディレクトリに置くことで、パッケージは複数のバイナリクレートを持つことができます。それぞれのファイルが別々のバイナリクレートになります。
クレートは、関連した機能を一つのスコープにまとめることで、その機能が複数のプロジェクト間で共有しやすいようにします。 例えば、2章で使ったrand
クレートは、乱数を生成する機能を提供します。 rand
クレートを私達のプロジェクトのスコープに持ち込むことで、この機能を私達のプロジェクトで使うことができます。 rand
クレートが提供する機能にはすべて、クレートの名前rand
を使ってアクセスできます。
クレートの機能をそれ自身のスコープの中に入れたままにしておくことは、ある機能が私達のクレートで定義されたのかrand
クレートで定義されたのかを明確にし、名前の衝突を予防してくれます。 例えば、rand
クレートはRng
という名前のトレイトを提供しています。 更に、私達のクレートでRng
という名前のstruct
を定義することもできます。 クレートの機能はそのスコープ内の名前空間に位置づけられているので、rand
を依存先として追加しても、コンパイラはRng
という名前が何を意味するのかについて混乱することはないのです。 私達のクレートでは、私達の定義したstruct Rng
のことであり、rand
クレートのRng
トレイトにはrand::Rng
でアクセスするというわけです。
では、モジュールシステムの話に移りましょう!