Progrust Library.

// 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円

// project

「Playgroundで開く」をクリックして修正・実行してください

2 files

Playgroundで開く
src/
main.rs
shop.rs
src/main.rs
// ここに1行足せ(shopの中身を src/shop.rs から読み込む)

fn main() {
    println!("合計: {}円", shop::total(&[980, 1250]));
}
src/shop.rs
pub fn total(prices: &[u32]) -> u32 {
    let mut sum = 0;
    for price in prices {
        sum += price;
    }
    sum
}
解答例と解説

// project

解答例

2 files

Playgroundで開く
src/
main.rs
shop.rs
src/main.rs
// ここに1行足せ(shopの中身を src/shop.rs から読み込む)
mod shop; 

fn main() {
    println!("合計: {}円", shop::total(&[980, 1250]));
}
src/shop.rs
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

totalpubが必要なことも、shop::total(..)というパスで呼ぶことも変わりません。ファイル分割は中身の置き場所を移すだけの操作で、可視性やパスの規則には一切影響しません。第13章・第14章で学んだことは、そのまま使えます。

mod#includeではない
C言語の#includeや他言語のimportと違い、modはファイルの内容をその場に貼り付ける操作ではありません。「このモジュールはツリーのこの位置にあります」という位置の宣言です。この違いは問題03ではっきりします。

02 - 子モジュールはディレクトリの中へ

モジュールのファイル分割パス(Path)に関する問題です。
shopの子モジュールcartsrc/shop/cart.rsに置いた2段構成です。2か所のコメントに従ってコードを完成させてください。

期待する出力
税込合計: 2453円

// project

「Playgroundで開く」をクリックして修正・実行してください

3 files

Playgroundで開く
src/
shop/
cart.rs
main.rs
shop.rs
src/main.rs
mod shop;

fn main() {
    println!("税込合計: {}円", shop::cart::total(&[980, 1250]));
}
src/shop.rs
// ここに1行足せ(cartの中身を src/shop/cart.rs から読み込む。外からも使えるようにすること)

// 非公開の定数(shopとその子孫からだけ使える)
const TAX_RATE: u32 = 10;
src/shop/cart.rs
pub fn total(prices: &[u32]) -> u32 {
    let mut sum = 0;
    for price in prices {
        sum += price;
    }

    // 親モジュールshopの非公開の定数TAX_RATEを使い、税込価格を返せ

}
解答例と解説

// project

解答例

3 files

Playgroundで開く
src/
shop/
cart.rs
main.rs
shop.rs
src/main.rs
mod shop;

fn main() {
    println!("税込合計: {}円", shop::cart::total(&[980, 1250]));
}
src/shop.rs
// ここに1行足せ(cartの中身を src/shop/cart.rs から読み込む。外からも使えるようにすること)
pub mod cart; 

// 非公開の定数(shopとその子孫からだけ使える)
const TAX_RATE: u32 = 10;
src/shop/cart.rs
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.rsshop/が並ぶ
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;と書く必要があります。cartmainからshop::cart::total(..)とたどられるので、第13章の問題03のとおり、途中のcartにもpubが要ります。ファイルへ分けても可視性の規則は変わりません。

superもそのまま効く
cart.rsから親のTAX_RATEsuper::TAX_RATEで参照できています。TAX_RATEは非公開ですが、第13章の問題06のとおり、子モジュールからは祖先の非公開の項目が見えます。ファイルが分かれてもツリー上の位置関係は変わらないので、第14章で学んだsupercrate::もそのまま使えます。ここでは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円

// project

「Playgroundで開く」をクリックして修正・実行してください

3 files

Playgroundで開く
src/
checkout.rs
main.rs
shop.rs
src/main.rs
mod checkout;
mod shop;

fn main() {
    checkout::pay(&[980, 1250]);
}
src/shop.rs
pub fn total(prices: &[u32]) -> u32 {
    let mut sum = 0;
    for price in prices {
        sum += price;
    }
    sum
}
src/checkout.rs
mod shop;

pub fn pay(prices: &[u32]) {
    println!("お支払い金額: {}円", shop::total(prices));
}
解答例と解説

// project

解答例

3 files

Playgroundで開く
src/
checkout.rs
main.rs
shop.rs
src/main.rs
mod checkout;
mod shop;

fn main() {
    checkout::pay(&[980, 1250]);
}
src/shop.rs
pub fn total(prices: &[u32]) -> u32 {
    let mut sum = 0;
    for price in prices {
        sum += price;
    }
    sum
}
src/checkout.rs
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と同じで、パスを最後まで書いてしまう手もあります。

useを書かない場合のsrc/checkout.rs
pub fn pay(prices: &[u32]) {
    println!("お支払い金額: {}円", crate::shop::total(prices));
}

shop::totalという書き方を保ちたいならuse crate::shop;、1回しか使わないならフルパス、という判断になります。

04 - ライブラリクレートとバイナリクレート

クレートパッケージに関する問題です。
次のプロジェクトにはsrc/main.rssrc/lib.rsがあります。src/lib.rstotal_with_shippingmainから呼び出してください。

Rust Playgroundのパッケージ名はplaygroundで固定されています。

期待する出力
お支払い金額: 2730円
お支払い金額: 3230円

// project

「Playgroundで開く」をクリックして修正・実行してください

2 files

Playgroundで開く
src/
lib.rs
main.rs
src/lib.rs
/// 送料込みの合計金額を返す(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 }
}
src/main.rs
fn main() {
    // ライブラリクレートのtotal_with_shippingを呼び出し「お支払い金額: 〇〇円」と出力せよ
    // 1回目の引数は &[980, 1250]、2回目は &[1980, 1250]

}
解答例と解説

// project

解答例

2 files

Playgroundで開く
src/
lib.rs
main.rs
src/lib.rs
/// 送料込みの合計金額を返す(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 }
}
src/main.rs
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.rsmain.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を書く場合のsrc/main.rs
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円

// project

「Playgroundで開く」をクリックして修正・実行してください

2 files

Playgroundで開く
src/
lib.rs
main.rs
src/lib.rs
/// 送料込みの合計金額を返す(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 }
}
src/main.rs
use crate::total_with_shipping;

fn main() {
    println!("お支払い金額: {}円", total_with_shipping(&[980, 1250]));
}
解答例と解説

// project

解答例

2 files

Playgroundで開く
src/
lib.rs
main.rs
src/lib.rs
/// 送料込みの合計金額を返す(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 }
}
src/main.rs
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で開く
解答例と解説

// project

解答例

4 files

Playgroundで開く
src/
shop/
cart.rs
checkout.rs
main.rs
shop.rs
src/main.rs
mod shop;

fn main() {
    shop::checkout::pay(&[980, 1250]);
}
src/shop.rs
pub mod cart;
pub mod checkout;

const TAX_RATE: u32 = 10;
src/shop/cart.rs
pub fn subtotal(prices: &[u32]) -> u32 {
    let mut sum = 0;
    for price in prices {
        sum += price;
    }
    sum
}
src/shop/checkout.rs
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_RATEshopが直接持っている定数なので、shop自身の中身であるsrc/shop.rsに残ります。

パスは1文字も変えていない
注目してほしいのは、checkoutの中のsuper::cart::subtotal(prices)super::TAX_RATEを、まったく書き換えていないことです。ファイルが3つに分かれても、モジュールツリーは元のままだからです。

crate
└── shop
    ├── TAX_RATE     // 非公開
    ├── cart
    │   └── subtotal
    └── checkout
        └── pay

checkoutから見てsupershopであること、shopの非公開のTAX_RATEが子から見えることも、すべて変わりません。ファイル分割はツリーの形に影響しないという、この章で繰り返し確かめてきた性質が、そのまま効いています。

pubの付け方も変わらない
cartcheckoutにはpubが必要で、TAX_RATEには要りません。元のコードで付いていたとおりに写せば、それが正解になります。逆に言えば、移し替えのときにpubを足したくなったら、それはツリーの形を変えてしまっているサインです。

どこまで分けるか
今回は練習のためにモジュール1つにつき1ファイルへ分けましたが、この規模なら1ファイルのままでもまったく問題ありません。分割はコードが読みづらい大きさになってから行うもので、早すぎる分割はファイルを行き来する手間が増えるだけです。まずインラインモジュールで構造を決め、育ってきたら切り出す、という順番が扱いやすいでしょう。