コンテンツにスキップ

Rustのドキュメントとドキュメンテーションテスト

docs.rs(英語)のどのページも、標準ライブラリのドキュメントのどのページも、プログラマーがソースコードの中に特別なコメントとして書いたものです。Rustは、コマンド1つでそのコメントをWebサイトに変えてくれます。自分のコードでも、まったく同じことができます。

気にかけるべき理由はもう1つあります。ドキュメントの中のコード例は本物のテストなのです。cargo test を実行すると、Rustはそれらをコンパイルして実行します。関数を変更して例を直し忘れると、テストが失敗します。ドキュメントが知らないうちに古くなってしまうことがないのです。これはプログラミング言語としては珍しく、Rustのとても良いところの1つです。

ドキュメンテーションコメント:/// と //!

Section titled “ドキュメンテーションコメント:/// と //!”

ふつうのコメントはRustのコメントのページで紹介しました。ドキュメンテーションコメント(doc comment)は見た目が似ていますが、記号が3文字です。

  • /// は、その下にある項目を説明します。関数、構造体、列挙型、フィールド、モジュールなどです。
  • //! は、自分が入っている項目を説明します。src/lib.rs や src/main.rs の先頭に書くと、クレート全体の説明になります。
//! 小さなレシピのプログラム。
/// `total` グラムを `portions` 人分に均等に分ける。
fn split_evenly(total: u32, portions: u32) -> u32 {
total / portions
}

ドキュメンテーションコメントの1行目は、短い要約の文にしましょう。ツールは一覧や検索結果にこの1行を表示するので、ここが大事です。

ドキュメンテーションコメントは、プログラムの動きを何も変えません。ドキュメントを付けた関数を main から使ってみましょう。

/// `total` グラムを `portions` 人分に均等に分ける。
///
/// 余ったグラムは切り捨てるので、10グラムを3人分に分けると3になる。
///
/// # Panics
///
/// `portions` が0のときはパニックする。
fn split_evenly(total: u32, portions: u32) -> u32 {
if portions == 0 {
panic!("portions は1以上でなければなりません");
}
total / portions
}
fn main() {
println!("{}", split_evenly(600, 3));
println!("{}", split_evenly(10, 3));
}

実行結果:

200
3
▶ 実際に試してみよう

たいていのエディタでは、main の中の split_evenly にマウスを乗せると、この説明がポップアップで表示されます。それだけでも、書く価値は十分にあります。

ドキュメンテーションコメントの中のMarkdown

Section titled “ドキュメンテーションコメントの中のMarkdown”

ドキュメンテーションコメントは、GitHubや多くのチャットアプリで使われているのと同じ Markdown で書きます。`code`、**太字**、*斜体*、リスト、リンク、見出しが使えます。

/// 料理に合ったオーブンの温度を、**摂氏**で返す。
///
/// 対応している料理:
///
/// - `"bread"`: 220度
/// - `"cake"`: 180度
/// - それ以外: 200度
///
/// オーブンによって違うので、*必ず*自分のオーブンで確かめること。
/// [セルシウス度(Wikipedia)](https://ja.wikipedia.org/wiki/セルシウス度)も参照。
pub fn oven_temperature(dish: &str) -> u32 {
match dish {
"bread" => 220,
"cake" => 180,
_ => 200,
}
}

段落と段落の間には、空の /// の行を入れます。ふつうのMarkdownファイルで空行を入れるのと同じです。関数名、型名、値はバッククォートで囲むと、コードとして表示されます。

Rustのプログラマーは、いくつかの見出しを何度も使います。そうすれば、読む人はどこを見ればよいかがわかります。そのうちの1つは、すでに上で見ました。よく使われるのは次のものです。日本語のドキュメントでも、これらの見出しは英語のまま書くことがよくあります。

  • # Examples:その項目の使い方です。公開している項目のほとんどに付けるべきです。
  • # Panics:その関数がパニックするのはどんなときか。呼び出す側が避けられるように書きます。
  • # Errors:Result を返す関数が Err を返すのはどんなときか、そしてその理由。
  • # Safety:unsafe fn の場合に、呼び出す側が守らなければならないルール(unsafe Rustを参照)。

ライブラリのファイル src/lib.rs で、# Errors と # Safety を使ってみましょう。

/// `"250g"` のような分量を読み取り、グラム数を返す。
///
/// # Errors
///
/// テキストが `g` で終わっていないとき、
/// または `g` の前の部分が整数でないときはエラーを返す。
pub fn parse_grams(text: &str) -> Result<u32, String> {
let number = text
.strip_suffix('g')
.ok_or(format!("'{text}' は g で終わる必要があります"))?;
number
.parse()
.map_err(|_| format!("'{number}' は整数ではありません"))
}
/// リストが空かどうかをチェックせずに、最初の材料を返す。
///
/// # Safety
///
/// 呼び出す側は、`ingredients` が空でないことを保証しなければならない。
/// 空のスライスに対して呼び出すと、未定義動作になる。
pub unsafe fn first_unchecked(ingredients: &[String]) -> &String {
unsafe { ingredients.get_unchecked(0) }
}

見出しには # を1つ使っている点に注目してください。ドキュメンテーションコメントの中では、# はRustの属性ではなく、Markdownの見出しです。

cargo doc でドキュメントをビルドする

Section titled “cargo doc でドキュメントをビルドする”

このページの残りでは、小さなライブラリを使って進めます。cargo new --lib kitchen で作りましょう。そして、プロジェクトの中で次のコマンドを実行します。

Terminal window
cargo doc --open
Documenting kitchen v0.1.0 (/path/to/kitchen)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.48s
Generated /path/to/kitchen/target/doc/kitchen/index.html

Cargoは target/doc/ にHTMLのWebサイトを作り、--open を付けるとそれをブラウザで開きます。見た目はdocs.rsのページとまったく同じです。サイドバー、検索ボックス、そして関数や型ごとのページがあります。依存しているクレートもすべてドキュメント化されるので、オフラインのときにも便利です。

デフォルトで表示されるのは、公開されている項目(pub が付いたもの)だけです。ドキュメントは、あなたのライブラリを使う人のためのものだからです。すべてを含めたいときは --document-private-items を付けます。

ドキュメンテーションテスト:実行される例

Section titled “ドキュメンテーションテスト:実行される例”

kitchen ライブラリの src/lib.rs の最初の部分です。関数には # Examples セクションがあり、ドキュメンテーションコメントの中にコードブロックが入っています。

//! キッチンで使う小さな便利関数。
//!
//! `kitchen` は、オーブンの温度を変換し、食べ物を何人分かに分け、
//! `"250g"` のような分量を読み取ります。
/// 温度を摂氏から華氏に変換する。
///
/// # Examples
///
/// ```
/// let f = kitchen::celsius_to_fahrenheit(180.0);
/// assert_eq!(f, 356.0);
/// ```
pub fn celsius_to_fahrenheit(celsius: f64) -> f64 {
celsius * 9.0 / 5.0 + 32.0
}

では、cargo test を実行してみましょう。

Doc-tests kitchen
running 1 test
test src/lib.rs - celsius_to_fahrenheit (line 10) ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Cargoは10行目のコードブロックを見つけ、小さなプログラムに変えて、コンパイルして実行しました。答えは assert_eq! がチェックしています。いくつか注目してほしい点があります。

  • 例では、クレート名を付けて kitchen::celsius_to_fahrenheit と呼び出しています。ドキュメンテーションテストは、あなたのライブラリを使う別の人のプログラムとしてコンパイルされます。そのため、見えるのは pub の項目だけで、フルパスで書くか、use kitchen::...; の行が必要です。
  • fn main は書きません。rustdocが例を main で包んでくれます。
  • assert! や assert_eq!(Rustのテストのページで紹介しました)を使うと、結果を見せることとチェックすることが同時にできます。

もし例が間違っていたらどうなるでしょうか。356.0 を 350.0 に変えて、もう一度テストを実行してみます。

running 1 test
test src/lib.rs - celsius_to_fahrenheit (line 10) ... FAILED
failures:
---- src/lib.rs - celsius_to_fahrenheit (line 10) stdout ----
Test executable failed (exit status: 101).
stderr:
assertion `left == right` failed
left: 356.0
right: 350.0
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.11s

これが狙いです。間違った例は、何か月もあとに読む人を混乱させる前に、すぐに見つかります。ドキュメンテーションテストだけを実行したいときは、cargo test --doc を使います。

例によっては、読む人の気が散るような行が必要になることがあります。ドキュメンテーションテストの中で # (シャープとスペース)で始まる行は、コンパイルはされますが、生成されたドキュメントには表示されません。

いちばんよく使うのは次の場面です。? 演算子の使い方を見せたいのに、? は Result を返す関数の中でしか使えません。そこで、自分で書いた main を隠します。

/// `"250g"` のような分量を読み取り、グラム数を返す。
///
/// # Examples
///
/// ```
/// use kitchen::parse_grams;
///
/// # fn main() -> Result<(), String> {
/// let flour = parse_grams("250g")?;
/// assert_eq!(flour, 250);
///
/// assert!(parse_grams("たくさん").is_err());
/// # Ok(())
/// # }
/// ```
pub fn parse_grams(text: &str) -> Result<u32, String> {
let number = text
.strip_suffix('g')
.ok_or(format!("'{text}' は g で終わる必要があります"))?;
number
.parse()
.map_err(|_| format!("'{number}' は整数ではありません"))
}

ブラウザで読む人に見えるのは、大事な4行だけです。use、? を使った呼び出し、そして2つの assert です。それでもテストでは、すべてがコンパイルされます。

すべての例がそのまま実行できるわけではありません。最初のバッククォートのあとに単語を書くと、rustdocにどう扱うかを伝えられます。

  • ```no_run:例をコンパイルはしますが、実行はしません。ファイルを読んだり、ネットワークにつないだり、永遠にループしたりするコードに向いています。
  • ```should_panic:実行して、パニックすることを期待します。パニックしなかったら、テストは失敗します。
  • ```ignore:まったくコンパイルしません。下書きや疑似コードに使います。まずは no_run を試しましょう。ignore した例は、誰にも気づかれないまま古くなってしまうことがあるからです。
  • ```text:Rustのコードではなく、ただのテキストです(たとえば、出力の例)。
/// `total` グラムを `portions` 人分に均等に分ける。
///
/// # Examples
///
/// ```
/// assert_eq!(kitchen::split_evenly(600, 3), 200);
/// ```
///
/// 0人分に分けるのは意味がないので、これはパニックする:
///
/// ```should_panic
/// kitchen::split_evenly(600, 0);
/// ```
pub fn split_evenly(total: u32, portions: u32) -> u32 {
if portions == 0 {
panic!("portions は1以上でなければなりません");
}
total / portions
}
/// 買い物リストを、1行に1品目ずつ書かれたファイルから読み込む。
///
/// ```no_run
/// let items = kitchen::load_list("shopping.txt").unwrap();
/// println!("買うものは {} 品", items.len());
/// ```
pub fn load_list(path: &str) -> std::io::Result<Vec<String>> {
let text = std::fs::read_to_string(path)?;
Ok(text.lines().map(String::from).collect())
}

これらをすべて kitchen に入れ、さらにクレートの //! コメントに ignore の下書きを1つ加えると、cargo test の結果は次のようになります。

running 7 tests
test src/lib.rs - (line 8) ... ignored
test src/lib.rs - load_list (line 87) - compile ... ok
test src/lib.rs - Recipe (line 104) ... ok
test src/lib.rs - celsius_to_fahrenheit (line 16) ... ok
test src/lib.rs - parse_grams (line 59) ... ok
test src/lib.rs - split_evenly (line 34) ... ok
test src/lib.rs - split_evenly (line 40) - should panic ... ok
test result: ok. 6 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 0.11s

no_run のテストには - compile と表示されています。ビルドしただけ、という意味です。テストは並列に実行されるので、行の順番は実行するたびに変わることがあります。

ドキュメンテーションコメントの中で、[`Vec`] のように名前をバッククォートと角かっこで囲むと、rustdocがその項目のページへのリンクに変えてくれます。これをドキュメント内リンク(intra-doc link)と呼びます。自分の項目にも、標準ライブラリのどんな項目にも使えます。

//! 小さなレシピのプログラム。
/// レシピ: 名前と、材料とそのグラム数のリスト。
///
/// [`Recipe::new`] で作り、[`Recipe::add`] で材料を追加する。
/// 材料は、追加した順に [`Vec`] に保存される。
struct Recipe {
/// レシピの一番上に表示される名前。
name: String,
/// それぞれの材料と、そのグラム数。
ingredients: Vec<(String, u32)>,
}
impl Recipe {
/// 指定した名前で、空のレシピを作る。
fn new(name: &str) -> Recipe {
Recipe { name: name.to_string(), ingredients: Vec::new() }
}
/// 材料をリストの最後に追加する。
fn add(&mut self, ingredient: &str, grams: u32) {
self.ingredients.push((ingredient.to_string(), grams));
}
/// すべての材料の重さを合計する。
fn total_grams(&self) -> u32 {
self.ingredients.iter().map(|(_, grams)| grams).sum()
}
}
fn main() {
let mut pancakes = Recipe::new("パンケーキ");
pancakes.add("小麦粉", 200);
pancakes.add("牛乳", 300);
pancakes.add("卵", 50);
println!("{}", pancakes.name);
for (ingredient, grams) in &pancakes.ingredients {
println!("- {ingredient}: {grams} g");
}
println!("合計: {} g", pancakes.total_grams());
}

実行結果:

パンケーキ
- 小麦粉: 200 g
- 牛乳: 300 g
- 卵: 50 g
合計: 550 g
▶ 実際に試してみよう

フィールドやメソッドにも、それぞれ /// コメントを付けられます。どれも構造体のページに表示されます。リンク先が存在しないときは、cargo doc が警告してくれます。次の例では、Recipe::add と書くつもりが Recipe::push と打ち間違えています。

warning: unresolved link to `Recipe::push`
--> src/lib.rs:100:27
|
100 | /// [`Recipe::new`] で作り、[`Recipe::push`] で材料を追加する。
| ^^^^^^^^^^^^ the struct `Recipe` has no field or associated item named `push`
|
= note: `#[warn(rustdoc::broken_intra_doc_links)]` on by default

警告の文はRust自身が表示するものなので英語です。「Recipe 構造体には push という名前のフィールドも関連項目もない」という意味です。

missing_docs でドキュメントを必須にする

Section titled “missing_docs でドキュメントを必須にする”

ほかの人が使うライブラリなら、ドキュメントのない公開項目をすべて、コンパイラに指摘してもらうことができます。src/lib.rs のいちばん先頭に、次の行を書きます。

#![warn(missing_docs)]

これで、ドキュメンテーションコメントを書き忘れると警告が出るようになります。

warning: missing documentation for a function
--> src/lib.rs:138:1
|
138 | pub fn grams_to_ounces(grams: f64) -> f64 {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
note: the lint level is defined here
--> src/lib.rs:1:9
|
1 | #![warn(missing_docs)]
| ^^^^^^^^^^^^

#!(感嘆符付き)は、その属性がクレート全体に適用されるという意味です。クレート自体にも //! のコメントを求めます。非公開の項目が指摘されることはないので、内部で使うだけの補助関数の負担にはなりません。

docs.rs:ドキュメントを無料で公開してくれる場所

Section titled “docs.rs:ドキュメントを無料で公開してくれる場所”

クレートをcrates.ioに公開すると(Cargoとクレートを参照)、docs.rs(英語)が自動的にそのドキュメントをビルドし、すべてのバージョンについて無料で公開してくれます。あなたがすることは何もありません。ほとんどのRustのクレートのドキュメントが同じ見た目で、検索しやすいのはこのためです。

ほかにも2つのコツがあります。

  • rustup doc --std を実行すると、標準ライブラリのドキュメントをオフラインでブラウザに開けます。
  • docs.rsや標準ライブラリのどのページでも、Source のリンクをクリックすると、関数の裏にあるコードを読めます。学ぶのにとても良い方法です。

ドキュメンテーションテストでクレート名を忘れる:ドキュメンテーションテストはクレートの外にあるので、次のように失敗します。

error[E0425]: cannot find function `celsius_to_fahrenheit` in this scope
--> src/lib.rs:17:9
|
17 | let f = celsius_to_fahrenheit(180.0);
| ^^^^^^^^^^^^^^^^^^^^^ not found in this scope

「このスコープに celsius_to_fahrenheit という関数が見つからない」というエラーです。直すには kitchen::celsius_to_fahrenheit(180.0) と書くか、例の先頭に use kitchen::celsius_to_fahrenheit; の行を加えます(必要なら # で隠しましょう)。

何も説明していないドキュメンテーションコメント:/// は、項目のすぐ前に書かなければなりません。関数の本体の中では // を使います。

error[E0585]: found a documentation comment that doesn't document anything
--> src/main.rs:3:5
|
3 | /// 食べ物を3人分に分ける。
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= help: doc comments must come before what they document, if a comment was intended use `//`

「何も説明していないドキュメンテーションコメントがある」というエラーで、ふつうのコメントのつもりなら // を使うように教えてくれています。

バイナリのドキュメンテーションテストが実行されると思い込む:ドキュメンテーションテストが実行されるのは、ライブラリのクレート(src/lib.rs)です。src/main.rs のドキュメンテーションコメントに書いた例はテストされません。プログラムが大きくなってきたら、ロジックを src/lib.rs に移し、main.rs は小さく保ちましょう。

コードをくり返すだけのドキュメントを書く:fn add(a, b) の上に「2つの数を足す」と書いても役に立ちません。コードからは見えないことを説明しましょう。単位(グラム?秒?)、境界のケース、そして失敗するのはどんなときかです。

  • /// は次の項目を、//! は自分が入っているクレートやモジュールを説明します。どちらもMarkdownで書きます。
  • # Examples、# Panics、# Errors、# Safety という定番のセクションを使いましょう。
  • cargo doc --open で、自分のクレートと依存クレートのHTMLサイトをビルドできます。
  • ドキュメンテーションコメントの中のコードブロックはドキュメンテーションテストです。cargo test がコンパイルして実行します。準備の行は # で隠し、必要に応じて no_run、should_panic、ignore を使います。
  • [`Name`] でドキュメント内リンクを作れます。#![warn(missing_docs)] を使うと、ドキュメントのない公開項目を指摘してもらえます。
  • crates.ioに公開したクレートのドキュメントは、docs.rsで自動的に公開されます。

理解度チェック

1. src/lib.rs の先頭に書いたとき、クレート全体を説明するコメントはどれですか?

2. ライブラリの /// コメントの中にあるコードブロックを、cargo test はどう扱いますか?

3. ドキュメンテーションテストの中で、# (シャープとスペース)で始まる行はどうなりますか?

4. ネットワークに接続する例があります。コンパイルはしたいけれど、実行はしたくありません。どの注釈を使いますか?

スコア: 0 / 4