// book chapter
動かして学ぶRustプログラミング問題集第15章 ファイル分割とクレート.
第13章と第14章では、modでモジュールツリーを組み、pubで公開範囲を決め、パスとuse宣言でその中の項目を指せるようになりました。ただし、ここまでのコードはすべて1つのファイルの中に書いてきました。
この章では、そのコードを複数のファイルへ分けます。使うのはmod shop;とセミコロンで終える書き方だけで、モジュールツリーの形もpubの効き方も、これまでと1つも変わりません。変わるのは「中身がどこに書いてあるか」だけです。
後半では、もう一段大きな単位であるクレートを扱います。1つのプロジェクトの中に、実行されるmainを持つバイナリクレートと、機能を提供するライブラリクレートを共存させられます。この2つは別のクレートなので、crate::が指す先も別になります。
この章からの出題形式
複数のファイルを扱うため、問題のコードは次のようなプロジェクトの形で出題します。
- 枠の上部に、ファイル数と「Playgroundで開く」ボタンが並びます
- その下にファイル構成のツリーが表示されます(ファイル名をクリックすると、そのコードブロックへ移動します)
- ボタンを押すとRust Playgroundが複数ファイルモードで開き、すべてのファイルがそのまま再現されます
ボタンはプロジェクト全体に1つだけ付きます。ファイルごとのボタンはありません。
進め方は第14章までと同じです。各問題の冒頭に関連する辞書へのリンクを挙げているので、まずはリンク先で必要な知識を確認してから取り組んでください。
01 - モジュールをファイルへ切り出す
モジュールのファイル分割とモジュールに関する問題です。
次のプロジェクトにはsrc/shop.rsが用意してあるのに、コンパイルエラー(E0433)になります。src/main.rsに1行足して修正してください。
合計: 2230円// ここに1行足せ(shopの中身を src/shop.rs から読み込む)
fn main() {
println!("合計: {}円", shop::total(&[980, 1250]));
}pub fn total(prices: &[u32]) -> u32 {
let mut sum = 0;
for price in prices {
sum += price;
}
sum
}解答例と解説
// ここに1行足せ(shopの中身を src/shop.rs から読み込む)
mod shop;
fn main() {
println!("合計: {}円", shop::total(&[980, 1250]));
}pub fn total(prices: &[u32]) -> u32 {
let mut sum = 0;
for price in prices {
sum += price;
}
sum
}エラーは第14章でも見たE0433で、use of unresolved module or unlinked crate 'shop'と言われています。src/shop.rsというファイルは確かに存在するのに、shopというモジュールは見つからない、という状態です。
ファイルを置いただけでは何も起きない
Rustのコンパイラはsrc/の中を探し回って、見つけたファイルを勝手に読み込んだりはしません。ファイルの中身をモジュールツリーに組み込むのは、あくまでmod宣言です。
モジュールのファイル分割は、mod shop;のように本体を持たずセミコロンで終える宣言で行います。これは「shopというモジュールがここにあります。中身は別のファイルにあるので読んできてください」という指示です。
| 書き方 | 意味 |
|---|---|
mod shop { ... } |
中身をその場に書く(インラインモジュール。第13章・第14章) |
mod shop; |
中身をsrc/shop.rsから読み込む |
今回のエラーには、次のヒントが添えられています。
help: to make use of source file src/shop.rs, use `mod shop` in this file to declare the module
「src/shop.rsを使いたいなら、このファイルにmod shopと書いて宣言せよ」という助言です。コンパイラはファイルの存在に気付いていて、それでも読み込まないのは、宣言がないからです。
ツリーの形は変わらない
ファイルを分けても、モジュールツリーは第13章までとまったく同じです。
crate // src/main.rs
├── shop // src/shop.rs
│ └── total
└── main
totalにpubが必要なことも、shop::total(..)というパスで呼ぶことも変わりません。ファイル分割は中身の置き場所を移すだけの操作で、可視性やパスの規則には一切影響しません。第13章・第14章で学んだことは、そのまま使えます。
modは#includeではない
C言語の#includeや他言語のimportと違い、modはファイルの内容をその場に貼り付ける操作ではありません。「このモジュールはツリーのこの位置にあります」という位置の宣言です。この違いは問題03ではっきりします。
02 - 子モジュールはディレクトリの中へ
モジュールのファイル分割とパス(Path)に関する問題です。
shopの子モジュールcartをsrc/shop/cart.rsに置いた2段構成です。2か所のコメントに従ってコードを完成させてください。
税込合計: 2453円mod shop;
fn main() {
println!("税込合計: {}円", shop::cart::total(&[980, 1250]));
}// ここに1行足せ(cartの中身を src/shop/cart.rs から読み込む。外からも使えるようにすること)
// 非公開の定数(shopとその子孫からだけ使える)
const TAX_RATE: u32 = 10;pub fn total(prices: &[u32]) -> u32 {
let mut sum = 0;
for price in prices {
sum += price;
}
// 親モジュールshopの非公開の定数TAX_RATEを使い、税込価格を返せ
}解答例と解説
mod shop;
fn main() {
println!("税込合計: {}円", shop::cart::total(&[980, 1250]));
}// ここに1行足せ(cartの中身を src/shop/cart.rs から読み込む。外からも使えるようにすること)
pub mod cart;
// 非公開の定数(shopとその子孫からだけ使える)
const TAX_RATE: u32 = 10;pub fn total(prices: &[u32]) -> u32 {
let mut sum = 0;
for price in prices {
sum += price;
}
// 親モジュールshopの非公開の定数TAX_RATEを使い、税込価格を返せ
sum * (100 + super::TAX_RATE) / 100
}shopの中身がsrc/shop.rsにあるように、shop::cartの中身はsrc/shop/cart.rsにあります。祖先にあたるモジュールがディレクトリになり、モジュール自身の中身は「モジュール名 + .rs」のファイルに入ります。
| モジュールパス | ファイルパス | そのファイルの中身 |
|---|---|---|
crate |
src/main.rs |
mod shop; |
crate::shop |
src/shop.rs |
pub mod cart; とTAX_RATE |
crate::shop::cart |
src/shop/cart.rs |
pub fn total |
パス(Path)をそのままファイルパスに写した形になっている、と考えると覚えやすいでしょう。::が/に変わり、最後のモジュールだけが.rsのファイルになります。
shop.rsとshop/が並ぶ
src/の直下にはshop.rsというファイルとshopというディレクトリが並びます。同じ名前で紛らわしく見えますが、役割が違うので両方あってよいのです。
src/
├── main.rs // crate
├── shop.rs // crate::shop 自身の中身
└── shop/ // crate::shop の子モジュールたちの置き場所
└── cart.rs // crate::shop::cart の中身
pubが要る理由
mod cart;ではなくpub mod cart;と書く必要があります。cartはmainからshop::cart::total(..)とたどられるので、第13章の問題03のとおり、途中のcartにもpubが要ります。ファイルへ分けても可視性の規則は変わりません。
superもそのまま効く
cart.rsから親のTAX_RATEをsuper::TAX_RATEで参照できています。TAX_RATEは非公開ですが、第13章の問題06のとおり、子モジュールからは祖先の非公開の項目が見えます。ファイルが分かれてもツリー上の位置関係は変わらないので、第14章で学んだsuperもcrate::もそのまま使えます。ここではcrate::shop::TAX_RATEと書いても同じ結果になります。
古い書き方: mod.rsスタイル
モジュールの中身は、モジュール名のディレクトリの中のmod.rsに置くこともできます。
| 内容 | src/shop.rsスタイル |
src/shop/mod.rsスタイル |
|---|---|---|
shop自身の中身 |
src/shop.rs |
src/shop/mod.rs |
子モジュールcart |
src/shop/cart.rs |
src/shop/cart.rs |
どちらも有効ですが、現在推奨されるのはsrc/shop.rsスタイルです。mod.rsスタイルだと、プロジェクト中にmod.rsという同名のファイルがいくつもでき、エディタでタブを並べたときに見分けが付かなくなるためです。既存のプロジェクトで見かけることはあるので、読めるようにしておけば十分です。
03 - modの宣言は1回だけ
モジュールのファイル分割とuse宣言に関する問題です。
次のプロジェクトはコンパイルエラー(E0583)になります。エラーメッセージを読み、src/checkout.rsの1行目を書き換えて修正してください。
お支払い金額: 2230円mod checkout;
mod shop;
fn main() {
checkout::pay(&[980, 1250]);
}pub fn total(prices: &[u32]) -> u32 {
let mut sum = 0;
for price in prices {
sum += price;
}
sum
}mod shop;
pub fn pay(prices: &[u32]) {
println!("お支払い金額: {}円", shop::total(prices));
}解答例と解説
mod checkout;
mod shop;
fn main() {
checkout::pay(&[980, 1250]);
}pub fn total(prices: &[u32]) -> u32 {
let mut sum = 0;
for price in prices {
sum += price;
}
sum
}mod shop;
use crate::shop;
pub fn pay(prices: &[u32]) {
println!("お支払い金額: {}円", shop::total(prices));
}エラーはfile not found for module 'shop'(E0583)です。src/shop.rsは確かにあるのに「見つからない」と言われるのが、この問題のつまずきどころです。
エラーメッセージには2行の補足が付いています。
help: to create the module `shop`, create file "src/checkout/shop.rs" or "src/checkout/shop/mod.rs"
note: if there is a `mod shop` elsewhere in the crate already, import it with `use crate::...` instead
1行目で、コンパイラが探した場所が分かります。src/shop.rsではなくsrc/checkout/shop.rsです。問題02のとおり、mod宣言が探すファイルの位置はその宣言を書いたモジュールからの相対で決まります。checkoutの中にmod shop;と書けば、それはcheckoutの子モジュールの宣言になり、対応するファイルはsrc/checkout/shop.rsになります。
そして2行目が、この問題の答えそのものです。「クレートのどこかにすでにmod shopがあるなら、代わりにuse crate::...で取り込め」と書かれています。
modは位置の宣言、useは名前の持ち込み
問題01で「modは#includeではない」と書いた理由がここにあります。modはファイルの中身を貼り付ける操作ではなく、モジュールツリーのどこに置くかを決める宣言です。1つのモジュールをツリーの2か所に同時に置くことはできないので、modは1つのモジュールにつき1回しか書けません。
すでに読み込まれているモジュールを別のファイルから使いたいときに書くのは、use宣言です。use crate::shop;はshopを新しく作るのではなく、すでにツリーにあるcrate::shopへの短い名前をこのファイルのスコープに増やすだけです。
| 書きたいこと | 書くもの | 何回書けるか |
|---|---|---|
| モジュールをツリーに登録する | mod shop; |
クレート全体で1回 |
| 登録済みのモジュールを使う | use crate::shop; |
使いたいスコープごとに何回でも |
useを書かない選択肢もある
第14章の問題04と同じで、パスを最後まで書いてしまう手もあります。
pub fn pay(prices: &[u32]) {
println!("お支払い金額: {}円", crate::shop::total(prices));
}shop::totalという書き方を保ちたいならuse crate::shop;、1回しか使わないならフルパス、という判断になります。
04 - ライブラリクレートとバイナリクレート
クレートとパッケージに関する問題です。
次のプロジェクトにはsrc/main.rsとsrc/lib.rsがあります。src/lib.rsのtotal_with_shippingをmainから呼び出してください。
Rust Playgroundのパッケージ名はplaygroundで固定されています。
お支払い金額: 2730円
お支払い金額: 3230円/// 送料込みの合計金額を返す(3000円以上は送料無料)
pub fn total_with_shipping(prices: &[u32]) -> u32 {
let mut sum = 0;
for price in prices {
sum += price;
}
if sum >= 3000 { sum } else { sum + 500 }
}fn main() {
// ライブラリクレートのtotal_with_shippingを呼び出し「お支払い金額: 〇〇円」と出力せよ
// 1回目の引数は &[980, 1250]、2回目は &[1980, 1250]
}解答例と解説
/// 送料込みの合計金額を返す(3000円以上は送料無料)
pub fn total_with_shipping(prices: &[u32]) -> u32 {
let mut sum = 0;
for price in prices {
sum += price;
}
if sum >= 3000 { sum } else { sum + 500 }
}fn main() {
// ライブラリクレートのtotal_with_shippingを呼び出し「お支払い金額: 〇〇円」と出力せよ
// 1回目の引数は &[980, 1250]、2回目は &[1980, 1250]
println!("お支払い金額: {}円", playground::total_with_shipping(&[980, 1250]));
println!("お支払い金額: {}円", playground::total_with_shipping(&[1980, 1250]));
}クレートは、コンパイラが一度に処理するコンパイルの単位です。種類は2つあります。
| 種類 | 入口のファイル | main関数 |
役割 |
|---|---|---|---|
| バイナリクレート | src/main.rs |
必要 | 実行可能ファイルになる |
| ライブラリクレート | src/lib.rs |
不要 | 機能を他から使わせる |
この2つのファイルを両方置くと、1つのパッケージの中にバイナリクレートとライブラリクレートが1つずつできます。ここまでの問題で書いてきたmod shop;のようなモジュールがクレートの内側の構造だったのに対して、クレートはその外側にある、もう一段大きな単位です。
別のクレートなので、名前で呼ぶ
shopのときはmod shop;と宣言してからshop::total(..)と呼びました。しかしlib.rsの中身にmod宣言は要りません。lib.rsはmain.rsの一部ではなく、別のクレートだからです。別のクレートを指すときは、そのクレート名をパスの起点に書きます。
playground(パッケージ)
├── playground(ライブラリクレート) // src/lib.rs
│ └── total_with_shipping
└── playground(バイナリクレート) // src/main.rs
└── main
ライブラリクレートの名前はパッケージ名から決まります。Rust Playgroundのパッケージ名はplayground固定なので、playground::total_with_shipping(..)と書けます。手元でcargo new shoppingとして作ったパッケージなら、shopping::total_with_shipping(..)になります。
useで短く書く
第14章で学んだuseはここでも使えます。
use playground::total_with_shipping;
fn main() {
println!("お支払い金額: {}円", total_with_shipping(&[980, 1250]));
}第14章の問題03では「関数は親モジュールまでで止める」という慣習を紹介しましたが、この形は例外的によく見かけます。裸のtotal_with_shipping(..)でも、useの行を見ればどのクレートのものかがすぐ分かるためです。
なぜ分けるのか
main.rsにすべて書いても動くのに、わざわざライブラリ側へ移すのには理由があります。
- 他のプログラムから使える。同じパッケージの中に実行ファイルを増やしても、共通のロジックは1か所で済みます
- テストしやすい。ライブラリクレートは
mainを持たないので、そのままテストの対象にできます - 公開できる。crates.ioへ公開できるのはライブラリクレートです
main.rsには引数の受け取りや結果の表示だけを書き、中身の処理はlib.rs側に置く、という分け方が定番です。
05 - crate::はどのクレートを指すか
クレートとパス(Path)に関する問題です。
問題04と同じ構成で、main.rsからcrate::でライブラリ側の関数を参照しようとしたところ、コンパイルエラー(E0432)になりました。パスを直して修正してください。
お支払い金額: 2730円/// 送料込みの合計金額を返す(3000円以上は送料無料)
pub fn total_with_shipping(prices: &[u32]) -> u32 {
let mut sum = 0;
for price in prices {
sum += price;
}
if sum >= 3000 { sum } else { sum + 500 }
}use crate::total_with_shipping;
fn main() {
println!("お支払い金額: {}円", total_with_shipping(&[980, 1250]));
}解答例と解説
/// 送料込みの合計金額を返す(3000円以上は送料無料)
pub fn total_with_shipping(prices: &[u32]) -> u32 {
let mut sum = 0;
for price in prices {
sum += price;
}
if sum >= 3000 { sum } else { sum + 500 }
}use crate::total_with_shipping;
use playground::total_with_shipping;
fn main() {
println!("お支払い金額: {}円", total_with_shipping(&[980, 1250]));
}エラーはunresolved import 'crate::total_with_shipping'(E0432)で、no 'total_with_shipping' in the rootと続きます。「ルートにそんなものはない」という指摘です。
第14章で、crateはクレートルートを指すと学びました。ここで問われるのはどのクレートのルートかです。答えは「今コンパイルしているクレート」で、main.rsに書いたcrate::はmain.rsをルートとするバイナリクレートを指します。ライブラリクレートは別のクレートなので、そちらのルートには届きません。
crate(main.rsから見たとき) crate(lib.rsから見たとき)
└── main └── total_with_shipping
crate::は「自分のクレートの中」を指す書き方なので、クレートの壁を越えることはできません。越えるときは、問題04のようにクレート名を起点にします。
| 起点 | 指す先 | どこから書けるか |
|---|---|---|
crate:: |
今コンパイルしているクレートのルート | そのクレートの中 |
playground:: |
playgroundという名前のクレートのルート |
そのクレートを使える場所 |
crate::が使えなくなるわけではない
バイナリ側にモジュールがあれば、main.rsでもcrate::は今までどおり使えます。問題03のuse crate::shop;がまさにそれで、shopはバイナリクレートの中のモジュールでした。ライブラリ側のファイル(lib.rsやそこから読み込むファイル)の中でcrate::と書けば、そちらはライブラリクレートのルートを指します。同じcrate::という記号が、書かれているファイルがどちらのクレートに属するかによって別の場所を指す、ということです。
直接パスで書いた場合
useを使わずにcrate::total_with_shipping(..)と直接書いた場合は、E0432ではなくE0425(cannot find function ... in the crate root)になります。エラーコードは違いますが、原因は同じです。
06 - 応用: 買い物カートを分割する
第15章の総復習です。
次のコードは、インラインモジュールで書かれた買い物カートです。出力を変えずに、下のファイル構成へ移し替えてください。
src/
├── main.rs
├── shop.rs
└── shop/
├── cart.rs
└── checkout.rs小計: 2230円
お支払い金額: 2453円mod shop {
const TAX_RATE: u32 = 10;
pub mod cart {
pub fn subtotal(prices: &[u32]) -> u32 {
let mut sum = 0;
for price in prices {
sum += price;
}
sum
}
}
pub mod checkout {
pub fn pay(prices: &[u32]) {
let subtotal = super::cart::subtotal(prices);
let total = subtotal * (100 + super::TAX_RATE) / 100;
println!("小計: {subtotal}円");
println!("お支払い金額: {total}円");
}
}
}
fn main() {
shop::checkout::pay(&[980, 1250]);
}Playgroundで開く解答例と解説
mod shop;
fn main() {
shop::checkout::pay(&[980, 1250]);
}pub mod cart;
pub mod checkout;
const TAX_RATE: u32 = 10;pub fn subtotal(prices: &[u32]) -> u32 {
let mut sum = 0;
for price in prices {
sum += price;
}
sum
}pub fn pay(prices: &[u32]) {
let subtotal = super::cart::subtotal(prices);
let total = subtotal * (100 + super::TAX_RATE) / 100;
println!("小計: {subtotal}円");
println!("お支払い金額: {total}円");
}移し替えの手順は機械的です。mod 名前 { ... }と書いてあるところをmod 名前;に置き換え、{}の中身をそのまま対応するファイルへ移すだけです。
| 元の位置 | 移した先 | 残るもの |
|---|---|---|
mod shop { ... }(クレートルート) |
src/shop.rs |
mod shop; |
pub mod cart { ... }(shopの中) |
src/shop/cart.rs |
pub mod cart; |
pub mod checkout { ... }(shopの中) |
src/shop/checkout.rs |
pub mod checkout; |
TAX_RATEはshopが直接持っている定数なので、shop自身の中身であるsrc/shop.rsに残ります。
パスは1文字も変えていない
注目してほしいのは、checkoutの中のsuper::cart::subtotal(prices)とsuper::TAX_RATEを、まったく書き換えていないことです。ファイルが3つに分かれても、モジュールツリーは元のままだからです。
crate
└── shop
├── TAX_RATE // 非公開
├── cart
│ └── subtotal
└── checkout
└── pay
checkoutから見てsuperがshopであること、shopの非公開のTAX_RATEが子から見えることも、すべて変わりません。ファイル分割はツリーの形に影響しないという、この章で繰り返し確かめてきた性質が、そのまま効いています。
pubの付け方も変わらない
cartとcheckoutにはpubが必要で、TAX_RATEには要りません。元のコードで付いていたとおりに写せば、それが正解になります。逆に言えば、移し替えのときにpubを足したくなったら、それはツリーの形を変えてしまっているサインです。
どこまで分けるか
今回は練習のためにモジュール1つにつき1ファイルへ分けましたが、この規模なら1ファイルのままでもまったく問題ありません。分割はコードが読みづらい大きさになってから行うもので、早すぎる分割はファイルを行き来する手間が増えるだけです。まずインラインモジュールで構造を決め、育ってきたら切り出す、という順番が扱いやすいでしょう。