Rustについて基本からしっかり学んでいく本連載。第12回は、Rustアプリケーションを構成するクレートやパッケージ、関数等の要素に論理的な階層構造を導入するモジュールについて。
この記事は会員限定です。会員登録(無料)すると全てご覧いただけます。
本連載で紹介してきたRustのサンプルコードは基本的に1つのソースファイルで完結していたので、ソースファイルを1つ作成→コンパイル→実行、という流れで何の問題もありませんでした。しかし、アプリケーションの規模が大きくなり、構成するソースファイルも増えてきて、ライブラリなども作成するとなってくると、これらをまとめて整理する仕組みが必要になります。それがモジュールシステムです。今回は、以下の表と図に示すRustのモジュールシステムの各要素を紹介します。
要素 | 概要 |
---|---|
パッケージ | Cargoパッケージマネージャで管理されるクレートの集合 |
クレート | パッケージ内の個々の実行バイナリとライブラリ |
モジュール | 関数などの要素の論理的な階層 |
パス | 関数などの要素やモジュールの論理的な場所を示す名前 |
表 モジュールシステムの各要素 |
パッケージとクレートは、アプリケーションを物理的に構成する要素です。パッケージ(package)は小包とか包み箱という意味で、1個以上のクレート(後述。ここでは単純にソースファイルと思ってください)をまとめたものをいいます。通常、パッケージはRustの備えるCargoパッケージマネージャ(以降、Cargo)によって作成され、ビルド、テストの対象となります。
Cargoの実体であるcargoコマンドによるパッケージの作成については、第1回で紹介しました。そこでも触れたように、パッケージではCargoの管理ファイルであるCargo.tomlによってアプリケーションの属性を指定するほか、ビルドの方法が管理されます。
本連載のサンプルのような学習用のアプリケーションであれば、Cargo.tomlファイルを触る機会はほとんどありません。ただし、広く配布するアプリケーションであれば正しくバージョン情報を設定する必要がありますし、複数のパッケージからなるアプリケーションを作成する場合には、Cargo.tomlファイルを積極的に触っていくことになります。
クレートはソースファイルなので、Rustのコンパイラであるrustcで個別にコンパイルすることは可能です。ただし、複数のファイルでアプリケーションが構成されたり、簡便にテストを実行したり、アプリケーションを配布したりするには、パッケージを利用した方が便利です。rustcはCargoから間接的に呼び出すことにして、通常はcargoコマンドによるパッケージ管理の機能を利用しましょう。
次はクレート(crate)です。クレートとは聞き慣れない言葉ですが、木枠とか箱という意味で、ソースコードを収納するファイルをこのように呼んでいます。これまで作成してきたソースファイルは、クレートというわけです。クレートには、実行バイナリ(アプリケーション)とライブラリという2つの種類があります。
cargo newコマンドで--binオプションを指定してパッケージを作成すると、実行バイナリのクレートが1個作成されます(--binオプションは省略可)。実行バイナリは、エントリポイント(main関数)を持つクレートです。なお--binは、実行バイナリの作成や実行を意味するオプションです。
% cargo new modules1 Created binary (application) `modules1` package ↑実行バイナリ(アプリケーション)パッケージ「modules1」が作成された
このとき、指定したパッケージ名と同名のフォルダ(ここではmodules1)が作成され、そこがパッケージの物理的なルートになります。このmodules1のsrcフォルダ以下にmain.rsファイルが自動的に作成され、そこには「Hello, world!」を出力するコードが書かれているということは第1回で紹介しました。
このsrc/main.rsがクレートであり、後述するモジュールの基点となるクレートということで、特にクレートルート(crate root)と呼ばれます。src/main.rsは、cargo runコマンドで特にオプションを与えずに実行されるクレート、すなわち暗黙のクレートでもあります。以下で、コンパイルされて実行しているのは、暗黙のクレートによる実行バイナリです。
% cd modules1 % ls src/main.rs src/main.rs ←作成された実行バイナリのクレートルート % cargo run ←コンパイルして実行 Compiling project v0.1.0 (/Users/nao/Documents/atmarkit_rust/modules1) Finished dev [unoptimized + debuginfo] target(s) in 1.55s Running `target/debug/modules1` ←暗黙のクレートはパッケージ名と同じ Hello, world!
第2回以降のサンプルのように、src/binフォルダ以下にsrc/main.rsとは別にクレートを作成することもできます。src/main.rsのように自動で作成する機能は用意されていないので、srcフォルダにbinフォルダを作成し、そこに「.rs」ファイルを作成していくことになります。そして、これらのクレートもクレートルートになります。ただしこのときは、cargo runコマンドに--binオプションを付けることで、クレートルートを明示して実行する必要があります。このように、プロジェクトにはsrc/main.rsをはじめとして、複数のクレートルートを持たせることができますが、実行できるのはそのうち1個だけです。
クレートには、ライブラリもあります。ライブラリは、エントリポイント(main関数)を持たないクレートです。cargo newコマンドに--libオプションを指定すると、src/lib.rsファイルが自動的に作成され、これもクレートルートとなります。
% cargo new --lib libraries Created library `libraries` package ↑ライブラリパッケージ「libraries」が作成された
ライブラリであるので、cargo runコマンドを実行してもコンパイルは実行されません。cargo buildコマンドで、ビルドすることになります。単独で作成したライブラリは、次回で紹介する予定の「ワークスペース」の機能を使って利用します。
% cd libraries % ls src/lib.rs src/lib.rs ←作成されたライブラリのクレートルート % cargo run error: a bin target must be available for `cargo run` ↑実行には実行バイナリが必要 % cargo build ←ライブラリのビルド Compiling lib_projects v0.1.0 (/Users/nao/Documents/atmarkit_rust/libraries) Finished dev [unoptimized + debuginfo] target(s) in 1.24s
ライブラリのクレートも、任意の場所に作成することもできます。例えば、実行バイナリのクレートであるsrc/main.rsが存在する状態でsrc/lib.rsを作成すると、実行バイナリとライブラリの2つのクレートルートを持つパッケージとなります。
モジュール(module)を使うと、クレート内の要素(関数等)に名前空間を付与でき、論理的な階層構造をアプリケーションに導入できます。
モジュールは、mod文で定義します。以下の例では、mod_functionクレートにgraphicsモジュールを定義し、その内部にcalcとdrawという2つのモジュールを定義しています。それぞれのモジュールの中には、get_x()、point()といった関数も定義しています(関数の処理内容自体は今回のテーマと無関係なので省略しています)。通常の関数定義を、mod文によるブロックで囲むわけです。また、モジュールの定義の中にさらに別のモジュールの定義を含めることができます。すなわち、階層構造を持った名前空間を定義できます。
// graphicsモジュールの開始 mod graphics { // calcモジュールの開始 mod calc { fn get_x() {} fn get_y() {} } // drawモジュールの開始 mod draw { fn point() {} fn line() {} fn triangle() {} fn square() {} } }
ここでは関数定義をモジュールに含めましたが、構造体、列挙体(enum)、定数、トレイトもモジュールに含めることができます。このうち、構造体をモジュールに含める例は、この節の最後で紹介します。
ところで、前節でクレートルートを紹介しました。src/main.rsとsrc/lib.rsというクレートルートは、暗黙のモジュールcrateを構成することになっています。また、クレートルートに定義された最も外側のモジュールも、crateに属することになっています。これはすなわち、crateモジュールをルートとしたモジュールツリーが構成される、ということになります。クレートルートという名前は、ここから来ているわけです。
crate └── graphics ├── calc │ ├── get_x │ └── get_y └── draw ├── point ├── line ├── circle └── square
モジュールを導入すると、このように名前空間を生成して、異なる名前空間にある要素を区別して扱うことができます。これは、要素を目的別に分けて管理する、ライブラリの整備に非常に有用です。また異なる名前空間には、同名のモジュールや要素を作成することができるので、すでに名前が使われているかどうかを調べたり、衝突を回避するために複雑な名前を付けたりする必要がなくなります。
続けて、パス(path)によるモジュールとその要素の参照方法を紹介します。例えば以下のsrc/bin/mod_function.rsは、モジュール内の関数を呼び出す例です。
…モジュール定義は省略… fn main() { // graphicsモジュールのdrawモジュールにあるpoint()関数を実行 point(); }
しかし、コンパイルエラーとなって実行できません。
error[E0425]: cannot find function `point` in this scope --> src/bin/mod_function.rs:15:5 | 15 | point(); | ^^^^^ not found in this scope
エラーメッセージの内容は、point()関数がスコープにない、というものです。このように、同一のクレートにあってもモジュールとして別の名前空間に定義された関数は、そのままでは呼び出すことはできません。
モジュールの参照には、パスを使います。パスは、ファイルシステムやURLといった、起点(ルート)と階層を持った構造と同じ考え方です。絶対パスと相対パスが存在する点も同じです。パス内のモジュールはダブルコロン(::)で区切ることになっています。
相対パスを指定するときには、self、superといったファイルシステムにおけるドット(.)やダブルドット(..)に相当するキーワードを使えます。それぞれ、同一階層、親階層を意味します。
src/bin/mod_function.rsを修正して、point()関数をパスを指定して呼び出してみた例が、以下です。
…モジュール定義は省略… fn main() { // 絶対パス crate::graphics::calc::get_x(); // 相対パス graphics::draw::point(); }
ただし、またしても、このファイルはコンパイルエラーとなります。
error[E0603]: module `calc` is private --> src/bin/module_path.rs:15:22 | 15 | crate::graphics::calc::get_x(); | ^^^^ private module …略… error[E0603]: module `draw` is private --> src/bin/module_path.rs:16:15 | 16 | graphics::draw::point(); | ^^^^ private module …略…
ここでは2つのエラーが発生しています。内容は同じく、モジュールがprivateであるというものです。privateとはここでは不可視という意味で、オブジェクト指向プログラミングにおけるprivateと同じです。Rustでは、単にモジュールを定義しただけではそのモジュールは不可視(private)です。不可視なモジュールは、基本的に外部から利用できません。これは、モジュールというものがコードの分離と再利用を目的としたものなので、定義したものが全て可視(public)なのは好ましくないためです。そこで、明示的にモジュールを可視にするアクセス制御が必要になりますが、この方法を続けて紹介します。
まず、どのようなときに可視になるのでしょうか? ある要素から見て同一階層のモジュールあるいは要素、上位階層のモジュールあるいは要素は可視です。すなわち、兄弟(sibling)と親(parent)は可視になります。src/bin/module_path.rsでいうと、square()関数とline()関数は同一階層にあるので、可視になります。逆に、square()関数からget_x()関数は不可視です。calcモジュールとdrawモジュールは兄弟ですが、その下にある関数等は兄弟にはならないことに注意が必要です。
不可視なモジュールあるいは要素を可視にするには、pubキーワードを使います。pubキーワードを定義に付記されたモジュールあるいは要素は、他のモジュールのどこからでも参照できます。モジュールを可視にしてもその配下の要素は不可視のままになるため注意が必要です。可視にしたい要素には、個別にpubキーワードを付記する必要があります。これは逆に言うと、細かなアクセス制御が可能になっている、ということです。
以下は、モジュールと関数の一部にpubキーワードを付記し、関数を3つ呼び出す例です。
…モジュール定義は省略… // graphicsモジュールはpublic pub mod graphics { // calcモジュールはprivate mod calc { // get_x()関数はpublic pub fn get_x() {} fn get_y() {} } // drawモジュールはpublic pub mod draw { // point()関数とline()関数はpublic pub fn point() {} pub fn line() {} fn triangle() {} fn square() {} } } fn main() { crate::graphics::calc::get_x(); // calcモジュールがprivate graphics::draw::point(); // 全てpublic graphics::draw::square(); // square()関数がprivate }
以下のように、コンパイルエラーになってしまいます。
error[E0603]: module `calc` is private --> src/bin/access_control.rs:16:22 | 16 | crate::graphics::calc::get_x(); | ^^^^ private module …略… error[E0603]: function `square` is private --> src/bin/access_control.rs:18:21 | 18 | graphics::draw::square(); | ^^^^^^ private function …略…
main()関数で、1番目と3番目の関数呼び出しがエラーとなっています。これらは、パスにあるモジュールおよび関数にprivateなものが含まれるためです。このように、モジュールや要素を外部から利用するときには、パス内の全てがpublicである必要があります。
構造体と列挙体をモジュール内部で定義すると、内部にあるフィールドやメソッドも全て不可視になります。これらを外部から参照したければ、フィールドとメソッドの定義にもpubキーワードを付記します。以下は、2つの構造体を定義し、その一部のフィールドとメソッドをpublicにする例です。
pub mod graphics { // graphicsモジュールはpublic struct Point { // Point構造体はprivate pub x: i32, pub y: i32, // xフィールドとyフィールドはpublic } pub struct Rect { // Rect構造体はpublic x: i32, y: i32, w: i32, h:i32, // これらのフィールドはprivate } } fn main() { let p = graphics::Point {x: 100, y: 100}; // エラー println!("Point = {}, {}", p.x, p.y); let r = graphics::Rect {x: 100, y: 100, w: 200, h: 300}; println!("Rect = {}, {}, {}, {}", r.x, r.y, r.w, r.h); // エラー }
出力例は省略しますが、このコードはコンパイルエラーとなります。Point構造体にはpubキーワードがないのでprivateになり、そのフィールドがpublicでも外部からはアクセスできません。Rect構造体はpublicですが、全てのフィールドがprivateなのでこれも外部からはアクセスできません。
構造体を単独で定義すると、全てのフィールドとメソッドがpublicになりますが、モジュールに含めることで不可視と可視の指定が可能になり、安全性が向上します。
今回は、Rustにおけるモジュールシステムの基本を紹介しました。関数や構造体に階層構造を持たせるモジュールによって、機能の分割が容易になることがお分かりいただけたのではないかと思います。
次回は、モジュールシステムの続きとして、モジュールをクレートに分割する方法、複数のプロジェクトをまとめるワークスペースを紹介します。
WINGSプロジェクト
有限会社 WINGSプロジェクトが運営する、テクニカル執筆コミュニティー(代表山田祥寛)。主にWeb開発分野の書籍/記事執筆、翻訳、講演等を幅広く手掛ける。2021年10月時点での登録メンバーは55人で、現在も執筆メンバーを募集中。興味のある方は、どしどし応募頂きたい。著書、記事多数。
・サーバーサイド技術の学び舎 - WINGS(https://wings.msn.to/)
・RSS(https://wings.msn.to/contents/rss.php)
・Twitter: @yyamada(https://twitter.com/yyamada)
・Facebook(https://www.facebook.com/WINGSProject)
Copyright © ITmedia, Inc. All Rights Reserved.