Rustのドキュメンテーション文化

プログラミング言語のドキュメンテーションには,しばしば二つの問題が伴う:

  1. ドキュメントを書く動機が弱い: 追加の作業が必要で,コードの変更に追従しなければならない

  2. ドキュメントとコードが乖離する: 時間とともにドキュメントが古くなり,実際のコードの振る舞いと一致しなくなる

Rustはこの両方の問題に対して,言語とツールチェーンのレベルで解決策を提供している.

ドキュメントコメント

Rustのドキュメントコメントは ///(アイテムの外側)または //!(アイテムの内側)で始まる:

/// 2 つの数値を加算する.
///
/// # Examples
///
/// ```
/// let result = my_crate::add(1, 2);
/// assert_eq!(result, 3);
/// ```
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}
//! # My Crate
//!
//! `my_crate` は数学的な操作を提供するライブラリです.
//! このドキュメントはクレートレベルの説明として機能します.

ドキュメントコメントはMarkdownで記述する.見出し,リスト,コードブロック,リンク,表 — Markdownのすべての構文が使える.

セクションの慣習

Rustのドキュメントには慣習的なセクションがある:

/// TCP ストリームからデータを読み取る.
///
/// # Arguments
///
/// * `buf` - データを格納するバッファ
///
/// # Returns
///
/// 読み取ったバイト数を返す.
///
/// # Errors
///
/// ソケットが閉じている場合,`io::Error` を返す.
///
/// # Panics
///
/// `buf` の長さが 0 の場合,パニックする.
///
/// # Safety
///
/// (unsafe 関数の場合) この関数を安全に呼ぶための前提条件を記述する.
///
/// # Examples
///
/// ```no_run
/// use std::net::TcpStream;
/// use std::io::Read;
///
/// let mut stream = TcpStream::connect("127.0.0.1:8080").unwrap();
/// let mut buf = [0u8; 1024];
/// let n = stream.read(&mut buf).unwrap();
/// ```
pub fn read(buf: &mut [u8]) -> io::Result<usize> {
    // ...
}
セクション 用途
# Examples 使用例(最も重要)
# Errors Resultを返す関数で,どのようなエラーが発生しうるか
# Panics パニックする可能性がある条件
# Safety unsafe関数の安全性に関する前提条件
# Arguments 引数の説明

ドキュメントテスト(Doc Tests)

Rustのドキュメンテーションで最も革新的な機能が ドキュメントテスト だ.

ドキュメントコメント内のコードブロック( で囲まれたもの)は,cargo test`実行時に **自動的にテストとして実行される**:

/// 数値が偶数かどうかを判定する.
///
/// # Examples
///
/// ```
/// assert!(my_crate::is_even(4));
/// assert!(!my_crate::is_even(3));
/// ```
pub fn is_even(n: i32) -> bool {
    n % 2 == 0
}
cargo test
   Doc-tests my_crate

running 1 test
test src/lib.rs - is_even (line 5) ... ok

test result: ok. 1 passed; 0 failed; 0 ignored

これがなぜ革新的か:

  1. ドキュメントがコードと乖離しない: ドキュメント内のコード例が実際にコンパイル・実行されるため,APIが変更されるとドキュメントテストが失敗する

  2. ドキュメントを書く動機が生まれる: ドキュメントコメントのコード例がそのままテストになるため,テストを書く行為とドキュメントを書く行為が一体化する

  3. サンプルコードの正しさが保証される: ユーザーがドキュメントのコード例をコピー&ペーストしたとき,それが動作することが保証される

ドキュメントテストのオプション

/// # Examples
///
/// コンパイルが通ることだけを確認(実行はしない):
/// ```no_run
/// let mut file = std::fs::File::create("output.txt").unwrap();
/// ```
///
/// コンパイルエラーになることを期待:
/// ```compile_fail
/// let x: i32 = "hello";
/// ```
///
/// テストとして実行しない(擬似コードや説明目的):
/// ```ignore
/// // この例は外部サービスが必要
/// let response = fetch_from_api().await;
/// ```
///
/// 隠れた行(ドキュメント表示では非表示,テストでは実行される):
/// ```
/// # use std::collections::HashMap;
/// # fn main() {
/// let mut map = HashMap::new();
/// map.insert("key", "value");
/// assert_eq!(map["key"], "value");
/// # }
/// ```

# で始まる行はドキュメントのHTML表示では非表示になるが,テスト実行時にはコードとして含まれる.これにより,use文やmain関数のボイラープレートを隠しつつ,テストとしては完全なコードを実行できる.

cargo doc

cargo docは,プロジェクト内のすべてのドキュメントコメントからHTMLドキュメントを生成する:

cargo doc --open

生成されるドキュメントには:

  • すべての公開アイテム(関数,構造体,enum,トレイト,モジュール)のAPIリファレンス

  • 型シグネチャ,トレイト実装の一覧

  • ドキュメントコメントのMarkdownレンダリング

  • ソースコードへのリンク

  • 検索機能

さらに,依存クレートのドキュメントも一緒に生成されるCargo.tomlに書かれたすべての依存クレートのドキュメントがローカルに生成され,クレート間のリンクも正しく解決される.

ドキュメント内リンク

ドキュメントコメント内で,他のアイテムへのリンクを書ける:

/// [`Vec`] と同様のインターフェースを持つ.
///
/// 詳細は [`Self::push`] を参照.
///
/// [`HashMap`](std::collections::HashMap) も参考にするとよい.
pub struct MyCollection {
    // ...
}

rustdocがこれらのリンクを自動的に解決し,正しいURLに変換する.リンク先が存在しない場合,コンパイル警告が出る.

docs.rs

docs.rsは,crates.ioに公開されたすべてのクレートのドキュメントを自動的にホスティングするサービスだ.

クレートをcargo publishすると,docs.rsが自動的にcargo docを実行し,ドキュメントを公開する.クレート作者が追加の作業を行う必要はない.

すべてのRustクレートがhttps://docs.rs/<crate-name> という統一的なURLでドキュメントにアクセスできる.これはNode.js (npm)やPython (PyPI)のエコシステムにはない,Rust独自のインフラだ.

ドキュメンテーションの質を支えるエコシステム

Clippyのドキュメント関連リント

Clippyには公開APIのドキュメントに関するリントも含まれている:

// clippy::missing_docs_in_private_items (pedantic)
// pub なアイテムにドキュメントコメントがない場合に警告

// clippy::missing_errors_doc
// Result を返す pub 関数に # Errors セクションがない場合に警告

// clippy::missing_panics_doc
// パニックしうる pub 関数に # Panics セクションがない場合に警告

#[doc]属性

ドキュメントコメントは実際には #[doc]属性のシンタックスシュガーだ:

/// これはドキュメントコメント
pub fn foo() {}

// ↑ これは以下と同等
#[doc = "これはドキュメントコメント"]
pub fn foo() {}

この事実を利用して,ドキュメントをプログラム的に生成することもできる:

macro_rules! make_documented_fn {
    ($name:ident, $doc:expr) => {
        #[doc = $doc]
        pub fn $name() {}
    };
}

make_documented_fn!(hello, "挨拶を行う関数.");

まとめ

Rustのドキュメンテーションシステムの素晴らしさは,個々の機能だけでなく,それらが統合されたエコシステムとして機能している点にある:

要素 役割
/// / //! Markdownによるドキュメントコメント
ドキュメントテスト コード例が自動的にテストされる
cargo doc HTMLドキュメントの生成
docs.rs 公開クレートのドキュメント自動ホスティング
Clippy ドキュメントの欠落を検出
ドキュメント内リンク アイテム間の参照を自動解決

この統合により,Rustエコシステムのドキュメントの質は他の言語と比較して著しく高い.crates.ioのクレートの多くが,充実したAPIドキュメントとテスト済みのコード例を持っている.

ドキュメンテーションはしばしば「書くべきだが書かれないもの」の代表例だ.Rustはこの問題に対して,「ドキュメントを書くことが自然にテストを書くことになる」という設計で解決した.正しさの保証と利便性を両立させる — これもまた,Rustの設計哲学の一つの表れだ.