// book chapter
動かして学ぶRustプログラミング問題集第14章 パスとuse宣言.
第13章で、modでコードをまとめ、pubで「どこまで見せるか」を決められるようになりました。この章では、そうやって組み上げたモジュールツリーの中の項目をどう指すかを6問で扱います。
第13章ではshop::cart::total(&prices)のように毎回フルパスを書いていました。これがパスです。パスにはcrateから書き始める形と現在地から書き始める形の2種類があり、親をたどるsuperという書き方もあります。そして、同じ長いパスを何度も書かずに済ませるための仕組みがuse宣言です。
第13章が「見えるかどうか」の話だったのに対して、この章は「どう指すか」の話です。2つは独立していて、パスが正しく書けていてもpubがなければ届きませんし、pubが付いていてもパスを間違えれば届きません。エラーメッセージのE0603(非公開)とE0433(見つからない)は、そのどちらでつまずいたかを表しています。
この章もインラインモジュールだけを扱うので、1つのコードブロックで完結します。
進め方は第13章までと同じです。各問題の冒頭に関連する辞書へのリンクを挙げているので、まずはリンク先で必要な知識を確認してから取り組んでください。
01 - 絶対パスと相対パス
パス(Path)に関する問題です。
同じ関数totalを、絶対パスと相対パスの2通りで呼び出してください。
絶対パス: 2230円
相対パス: 2230円mod shop {
pub mod cart {
pub fn total(prices: &[u32]) -> u32 {
let mut sum = 0;
for price in prices {
sum += price;
}
sum
}
}
}
fn main() {
let prices = [980, 1250];
// 絶対パスでtotalを呼び出し「絶対パス: 〇〇円」と出力せよ
// 相対パスでtotalを呼び出し「相対パス: 〇〇円」と出力せよ
}Playgroundで開く解答例と解説
mod shop {
pub mod cart {
pub fn total(prices: &[u32]) -> u32 {
let mut sum = 0;
for price in prices {
sum += price;
}
sum
}
}
}
fn main() {
let prices = [980, 1250];
// 絶対パスでtotalを呼び出し「絶対パス: 〇〇円」と出力せよ
// crateから書き始める
println!("絶対パス: {}円", crate::shop::cart::total(&prices));
// 相対パスでtotalを呼び出し「相対パス: 〇〇円」と出力せよ
// 現在地にあるshopから書き始める
println!("相対パス: {}円", shop::cart::total(&prices));
}Playgroundで開くパス(Path)は、::で区切った名前を並べてモジュールツリー上の項目をたどる書き方です。起点の違いで2種類に分かれます。
| 種類 | 起点 | 書き出し |
|---|---|---|
| 絶対パス | クレートルート | crate:: |
| 相対パス | 現在いるモジュール | その場から見える名前 |
ファイルのパスによく似ています。絶対パスは/home/user/memo.txtのようにルートから書く形、相対パスはmemo.txtのように今いる場所から書く形です。
今回はmainがクレートルートにいるので、どちらも同じshopから始まります。crate::を付けるか付けないかだけの違いに見えますが、mainが別のモジュールの中にあれば相対パスのほうは書けなくなります。相対パスは書いている場所によって変わるというのが本質的な違いです。
どちらを使うべきか
The Rust Programming Languageは、一般に絶対パスを勧めています。理由は、コードを別の場所へ移したときにどちらが壊れるかにあります。
| 移動のしかた | 絶対パス | 相対パス |
|---|---|---|
| 呼び出す側だけを別のモジュールへ移す | そのまま使える | 書き直しが必要 |
| 定義と呼び出す側をまとめて移す | 書き直しが必要 | そのまま使える |
定義と呼び出しは別々に動かすことのほうが多いので、既定は絶対パスにしておくと安全です。逆に「この2つは必ず一緒に動く」と言い切れるまとまりでは相対パスが有利で、その典型が問題06で扱うテストモジュールです。
02 - superで親をたどる
superとパス(Path)に関する問題です。
次のコードはコンパイルエラー(E0433)になります。パスを直して修正してください。
お支払い金額: 2230円mod shop {
pub mod cart {
pub fn total(prices: &[u32]) -> u32 {
let mut sum = 0;
for price in prices {
sum += price;
}
sum
}
}
pub mod checkout {
pub fn pay(prices: &[u32]) {
let total = cart::total(prices);
println!("お支払い金額: {total}円");
}
}
}
fn main() {
shop::checkout::pay(&[980, 1250]);
}Playgroundで開く解答例と解説
mod shop {
pub mod cart {
pub fn total(prices: &[u32]) -> u32 {
let mut sum = 0;
for price in prices {
sum += price;
}
sum
}
}
pub mod checkout {
pub fn pay(prices: &[u32]) {
let total = cart::total(prices);
let total = super::cart::total(prices);
println!("お支払い金額: {total}円");
}
}
}
fn main() {
shop::checkout::pay(&[980, 1250]);
}Playgroundで開くエラーはfailed to resolve: use of unresolved module or unlinked crate 'cart'(E0433)です。「cartというモジュールが見つからない」と言われています。
cartとcheckoutはどちらもshopの中にある兄弟モジュールです。同じ親を持っているので隣同士のように見えますが、checkoutの中から相対パスで書き始めたときに見えるのは、checkout自身の中にあるものだけです。cartは1つ上のshopの中にあるので、そのままでは見つかりません。
crate
└── shop
├── cart // 目的地
└── checkout // ここから書き始めている
superは、パスの先頭に置いて親モジュールを起点にするキーワードです。super::cart::totalと書けば、checkoutから1つ上のshopへ上がり、そこからcartをたどることになります。
| 先頭に書く語 | 起点 | ファイルパスでの例え |
|---|---|---|
crate |
クレートルート | / |
self |
現在のモジュール | ./ |
super |
親モジュール | ../ |
絶対パスでも書ける
crate::shop::cart::total(prices)と書いても同じ結果になります。問題01の基準に従えば、cartとcheckoutを切り離して別の場所へ移す可能性があるなら絶対パスのほうが安全です。逆にshopごとまとめて移す前提なら、super::のほうが移動後もそのまま動きます。
superは重ねられる
super::super::と連ねれば、さらに上の祖先までたどれます。ただしクレートルートには親がないので、そこでsuperを書くとE0433になります。ファイルパスで/より上へ行けないのと同じです。
03 - use宣言で持ち込む
use宣言に関する問題です。
次のコードはshop::cart::の繰り返しが目立ちます。use宣言を1行足して、cart::add・cart::totalと短く書けるようにしてください。出力は変えません。
1点目を追加しました
2点目を追加しました
合計: 2230円mod shop {
pub mod cart {
pub fn add(prices: &mut Vec<u32>, price: u32) {
prices.push(price);
println!("{}点目を追加しました", prices.len());
}
pub fn total(prices: &[u32]) -> u32 {
let mut sum = 0;
for price in prices {
sum += price;
}
sum
}
}
}
// ここにuse宣言を1行足せ
fn main() {
let mut prices = Vec::new();
// 下の3行を cart::add / cart::total と書けるように直せ
shop::cart::add(&mut prices, 980);
shop::cart::add(&mut prices, 1250);
println!("合計: {}円", shop::cart::total(&prices));
}Playgroundで開く解答例と解説
mod shop {
pub mod cart {
pub fn add(prices: &mut Vec<u32>, price: u32) {
prices.push(price);
println!("{}点目を追加しました", prices.len());
}
pub fn total(prices: &[u32]) -> u32 {
let mut sum = 0;
for price in prices {
sum += price;
}
sum
}
}
}
// ここにuse宣言を1行足せ
use crate::shop::cart;
fn main() {
let mut prices = Vec::new();
// 下の3行を cart::add / cart::total と書けるように直せ
shop::cart::add(&mut prices, 980);
shop::cart::add(&mut prices, 1250);
println!("合計: {}円", shop::cart::total(&prices));
cart::add(&mut prices, 980);
cart::add(&mut prices, 1250);
println!("合計: {}円", cart::total(&prices));
}Playgroundで開くuse宣言は、パスをその場のスコープに持ち込む宣言です。use crate::shop::cart;と一度書いておけば、以降そのスコープではcartという名前がcrate::shop::cartを指すようになります。
ショートカットを作る操作だと考えると分かりやすいでしょう。実体が移動するわけではなく、その場から使える短い名前が1つ増えるだけです。
どこまで持ち込むかには慣習がある
use crate::shop::cart::total;と関数そのものまで持ち込めば、total(&prices)とさらに短く書けます。しかしThe Rust Programming Languageは、関数は親モジュールまでで止めることを勧めています。
use crate::shop::cart;
cart::total(&prices); // どこか別の場所で定義された関数だと分かるuse crate::shop::cart::total;
total(&prices); // このファイルで定義した関数のように見えてしまう裸の名前で呼べるようにすると、そのファイルで定義した関数なのか、よそから持ち込んだ関数なのかが読み取れなくなります。親モジュールまでで止めれば、繰り返しは減らしつつ出どころも残せます。
一方、構造体・列挙型などの型はそれ自体まで持ち込むのが慣習です。use std::collections::HashMap;と書いてHashMap::new()と呼ぶ形で、こちらは標準ライブラリのドキュメントでもよく見かけます。
| 対象 | 持ち込む深さ | 呼び出しの形 |
|---|---|---|
| 関数 | 親モジュールまで | cart::total(..) |
| 構造体・列挙型など | 項目そのものまで | HashMap::new() |
まとめ書きと別名
useには便利な書き方がいくつかあります。
| 書き方 | 意味 |
|---|---|
use std::cmp::Ordering as Ord2; |
asで名前を変えて持ち込む |
use std::{cmp, io}; |
波括弧で同じ接頭辞のものをまとめて持ち込む |
use std::io::{self, Write}; |
波括弧の中のselfで接頭辞のio自身も持ち込む |
use std::io::prelude::*; |
*でその先の項目をすべて持ち込む |
asは問題05、*は問題06で実際に使います。
04 - useが効くのはそのスコープだけ
use宣言とパス(Path)に関する問題です。
次のコードはuse宣言を書いてあるのにコンパイルエラー(E0433)になります。checkoutモジュールの中でもcartを使えるように修正してください。
合計: 2230円
お支払い金額: 2230円mod shop {
pub mod cart {
pub fn total(prices: &[u32]) -> u32 {
let mut sum = 0;
for price in prices {
sum += price;
}
sum
}
}
}
use crate::shop::cart;
mod checkout {
pub fn pay(prices: &[u32]) {
println!("お支払い金額: {}円", cart::total(prices));
}
}
fn main() {
println!("合計: {}円", cart::total(&[980, 1250]));
checkout::pay(&[980, 1250]);
}Playgroundで開く解答例と解説
mod shop {
pub mod cart {
pub fn total(prices: &[u32]) -> u32 {
let mut sum = 0;
for price in prices {
sum += price;
}
sum
}
}
}
use crate::shop::cart;
mod checkout {
use crate::shop::cart;
pub fn pay(prices: &[u32]) {
println!("お支払い金額: {}円", cart::total(prices));
}
}
fn main() {
println!("合計: {}円", cart::total(&[980, 1250]));
checkout::pay(&[980, 1250]);
}Playgroundで開くエラーは問題02と同じE0433で、cartが見つからないと言われています。mainからはcart::totalと書けているのに、checkoutの中では書けません。
use宣言が作る名前が有効なのは、それを書いたスコープの中だけです。ファイルの先頭に書いたからといって、そのファイル全体に効くわけではありません。今回のuseはクレートルートに書かれているので、有効なのはクレートルート(つまりmainなど、モジュールで囲まれていない場所)だけです。
crate
├── use crate::shop::cart // この名前が有効なのはクレートルートの中だけ
├── shop
├── checkout // ここには届かない
└── main // ここでは使える
mod checkout { ... }は新しいスコープなので、そこでcartという名前を使いたければ、そのモジュールの中でもう一度useを書きます。
書かずに済ませる方法もある
useを足す以外に、パスを最後まで書いてしまう手もあります。
mod checkout {
pub fn pay(prices: &[u32]) {
// 絶対パス
println!("お支払い金額: {}円", crate::shop::cart::total(prices));
// 相対パス(親であるクレートルートへ上がってからたどる)
println!("お支払い金額: {}円", super::shop::cart::total(prices));
}
}同じモジュール内で1回しか使わないならフルパスのまま書き、何度も出てくるならuseを足す、という判断になります。
なぜスコープ単位なのか
useがファイル全体に効いてしまうと、モジュールの中身が外側の宣言に左右されることになります。第13章で見たとおり、モジュールは境界を作るための仕組みです。useもその境界に従うので、モジュールの中を読むときは「そのモジュールのuseだけ見れば、どこから何を持ち込んでいるか分かる」状態が保たれます。
05 - asで別名を付ける
use宣言に関する問題です。
次のコードはコンパイルエラー(E0252)になります。use宣言の書き方を変えて修正してください。
現金払い: 2230円
カード払い: 2280円mod payment {
pub mod cash {
pub fn total(prices: &[u32]) -> u32 {
let mut sum = 0;
for price in prices {
sum += price;
}
sum
}
}
pub mod card {
pub fn total(prices: &[u32]) -> u32 {
let mut sum = 0;
for price in prices {
sum += price;
}
sum + 50 // カード手数料
}
}
}
use crate::payment::cash::total;
use crate::payment::card::total;
fn main() {
let prices = [980, 1250];
println!("現金払い: {}円", total(&prices));
println!("カード払い: {}円", total(&prices));
}Playgroundで開く解答例と解説
mod payment {
pub mod cash {
pub fn total(prices: &[u32]) -> u32 {
let mut sum = 0;
for price in prices {
sum += price;
}
sum
}
}
pub mod card {
pub fn total(prices: &[u32]) -> u32 {
let mut sum = 0;
for price in prices {
sum += price;
}
sum + 50 // カード手数料
}
}
}
use crate::payment::cash::total;
use crate::payment::card::total;
use crate::payment::cash::total as cash_total;
use crate::payment::card::total as card_total;
fn main() {
let prices = [980, 1250];
println!("現金払い: {}円", total(&prices));
println!("カード払い: {}円", total(&prices));
println!("現金払い: {}円", cash_total(&prices));
println!("カード払い: {}円", card_total(&prices));
}Playgroundで開くエラーはthe name 'total' is defined multiple times(E0252)です。
use宣言はスコープに名前を作る操作なので、同じ名前を2回持ち込めば衝突します。同じファイルにfn totalを2つ書けないのと同じことが、持ち込んだ名前でも起きています。しかも仮に通ったとしても、total(&prices)と書いたときにどちらを呼びたいのかコンパイラには分かりません。
解決策の1つがasで、持ち込む名前を好きなものに変えられます。use crate::payment::cash::total as cash_total;と書けば、この先cash_totalという名前でcash::totalを呼べます。
そもそも持ち込みすぎ、という見方もある
この問題はもう1つの直し方でも解決できます。問題03の慣習どおり、関数は親モジュールまでで止める書き方です。
mod payment {
pub mod cash {
pub fn total(prices: &[u32]) -> u32 {
let mut sum = 0;
for price in prices {
sum += price;
}
sum
}
}
pub mod card {
pub fn total(prices: &[u32]) -> u32 {
let mut sum = 0;
for price in prices {
sum += price;
}
sum + 50 // カード手数料
}
}
}
use crate::payment::{card, cash}; // 波括弧でまとめて持ち込む
fn main() {
let prices = [980, 1250];
println!("現金払い: {}円", cash::total(&prices));
println!("カード払い: {}円", card::total(&prices));
}Playgroundで開くこちらは別名を考える必要がなく、cash::total・card::totalという呼び方に「どちらの計算なのか」がそのまま残ります。E0252に出会ったら、asで別名を付ける前に「持ち込む深さが深すぎないか」を先に疑ってみてください。
なお、paymentの中の2つのモジュールをまとめて持ち込むuse crate::payment::{card, cash};が、問題03の表で挙げた波括弧の書き方です。接頭辞のcrate::payment::を1回だけ書けばよく、use宣言が2行に増えるのを防げます。
asが本当に必要になる場面
名前が同じで、しかも両方を項目まで持ち込みたいときです。標準ライブラリのstd::fmt::Resultとstd::io::Resultが代表例で、どちらもResultという名前なので、片方にuse std::io::Result as IoResult;のように別名を付けて使い分けます。
06 - テストモジュールとuse super::*
superとuse宣言に関する問題です。
次のコードは、TESTを実行するとコンパイルエラー(E0425)になります。testsモジュールに1行足して、テストに合格させてください。
fn with_tax(price: u32) -> u32 {
price * 110 / 100
}
fn discount(price: u32, off: u32) -> u32 {
price - off
}
#[cfg(test)]
mod tests {
// ここに1行足せ(with_taxとdiscountを、このモジュールの中で呼べるようにする)
#[test]
fn test_with_tax() {
assert_eq!(with_tax(1000), 1100);
assert_eq!(with_tax(2230), 2453);
}
#[test]
fn test_discount() {
assert_eq!(discount(1000, 200), 800);
}
}Playgroundで開く解答例と解説
fn with_tax(price: u32) -> u32 {
price * 110 / 100
}
fn discount(price: u32, off: u32) -> u32 {
price - off
}
#[cfg(test)]
mod tests {
// ここに1行足せ(with_taxとdiscountを、このモジュールの中で呼べるようにする)
use super::*;
#[test]
fn test_with_tax() {
assert_eq!(with_tax(1000), 1100);
assert_eq!(with_tax(2230), 2453);
}
#[test]
fn test_discount() {
assert_eq!(discount(1000, 200), 800);
}
}Playgroundで開くuse super::*;は、Rustのテストコードで最も定番の1行です。3つの要素の組み合わせでできています。
superで親モジュールを指す
mod testsはクレートルートの子モジュールなので、superはクレートルート、つまりwith_taxとdiscountが置かれている場所です。ここでは絶対パスのuse crate::*;と書いても同じ結果になりますが、テストモジュールは対象と必ず一緒に動くので、問題01の基準どおり相対パスのsuperを使うのが定番になっています。
*でまとめて持ち込む
use super::with_tax;とuse super::discount;を並べても動きますが、テスト対象が増えるたびにuseを足すことになります。glob演算子の*は「その先の持ち込めるものを全部」という意味で、テストモジュールはまさに全部使いたい場所です。
問題03で触れたとおり*は普段は避けるべき書き方ですが、テストモジュールは数少ない例外として推奨されています。取り込む相手が自分の親モジュールに限られていて、出どころを見失う心配がないためです。
非公開のままテストできる
with_taxにもdiscountにもpubが付いていません。それでもテストから呼べるのは、第13章の問題06で確かめたとおり、子モジュールからは祖先の非公開の項目が見えるためです。testsはクレートルートの子なので、そこにある非公開の関数がすべて見えます。
Rustで「テストのためにpubにする」必要がないのは、この規則のおかげです。公開範囲を広げずに内部の関数を直接テストできます。
#[cfg(test)]は何をしているか
#[cfg(test)]は「テストとしてビルドするときだけ、この項目をコンパイルする」という指定です。付けておけば、cargo buildで作られる成果物にテストコードは一切含まれません。テスト用のデータや補助関数を気軽に置けるのはこのためです。
#[test]のほうは個々のテスト関数に付ける印で、こちらは第4章から使ってきました。2つはセットで、#[cfg(test)] mod tests { ... }の中に#[test]付きの関数を並べるのが標準的な形です。