rust-implement について
設計、実装、簡素化、検証の段階に分けて進める Rust 開発者向けスキルです。型設計、エラー処理、所有権、非同期処理、リソース解放の指針を扱います。
主な機能
- 01実装前の型と関数シグネチャ設計
- 02エラー処理と所有権の指針
- 03非同期処理とリソース解放の規則
- 04実装を簡素化するレビュー
- 05Lint と手動チェック
活用シーン
- 01コーディング前の Rust モジュール設計
- 02エラー処理と非同期クリーンアップのレビュー
- 03実装規約に沿ったコード確認
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ pproenca/ dot-skills --skill rust-implementインストール後、Claude Code で/rust-implementを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
AGENTS.mdmetadata.jsonscripts/lint.sh詳細
Rust 実装の規律
コードは段階的に書きます。熟練者でも、完璧なコードを一度で生み出すわけではありません。設計し、実装し、レビューし、簡潔にします。作成するすべてのモジュールで、このプロセスに従ってください。
パス 1:型とシグネチャを設計する
実装を書く前に、型定義と関数シグネチャだけを書きます。本文もロジックも書きません。
次に進む前に、以下の質問を自問してください。
- enum の状態で無効な組み合わせを表現できてしまいませんか?できる場合は、無効な状態を表現できないように構造を見直してください。
- すべてのパラメーターは、それ自体で意味が分かる名前になっていますか?
boolパラメーターは enum に置き換えてください(変換 2)。 - シリアライズまたは比較するすべての構造体で、
HashMapではなくBTreeMapを使っていますか(変換 3)? - 設定用の構造体は
Defaultを derive していますか(変換 6)? Stringパラメーターの順序を呼び出し元が取り違える可能性はありませんか?newtype を使ってください。- 一度に有効な組み合わせが 1 つだけの
Option<T>フィールドを 2 つ以上持つ構造体はありませんか?enum に変換してください(変換 8)。 - パラメーターを 4 つ以上取る関数はありませんか?引数用の構造体に置き換えてください(変換 9)。
型が適切になるまで、パス 2 に進まないでください。型がアーキテクチャを形作ります。
パス 2:実装する
関数の本文を記述します。各関数を書く際は、次のルールを適用してください。
- すべての
?に.context("failed to [verb] [noun]")を付けます。例外はありません(変換 1)。 - ライブラリコードでは、エラー enum に
thiserrorを使います。アプリケーションコードでは、anyhowを使います。 - 関数が借用する場合と割り当てる場合があるときは、
Cow<str>を使います(変換 7)。 - 戻り値が 2 つ以上の関数では、タプルではなく名前付き構造体を返します(変換 5)。
matchアームはすべて網羅します。ワイルドカードの_ =>は使いません(変換 4)。
フィールドの引き回し
実行箇所まで渡す必要がある新しいフィールドを追加するときは、コードを書く前に、値が通る経路全体をたどり、途中にあるすべてのレイヤーを明示してください。各段階で、その値を受け渡すモジュールと構造体/関数を示します。途中のレイヤーを飛ばすと、実行時にフィールドが気付かないまま失われます。
Drop/終了処理の精度
.awaitの前にロックガードを解放してください。await 中にMutexGuardを保持すると、エグゼキューターをブロックします。Drop実装では stdin を明示的に閉じてください。短時間待機してから、必要に応じて強制終了します。- 親より長く存続してはならない子プロセスには、
kill_on_drop(true)を使います。 - クリーンアップの順序を決定論的にします。リスナーをクリア -> タスクを排出 -> リソースを破棄。
防御的なシリアライズ
- SQLite の書き込みトランザクションでは
BEGINではなくBEGIN IMMEDIATEを使います(SQLITE_BUSY_SNAPSHOTを防ぎます)。 - リトライを追加するのではなく、ロックの取得を順序付けます。
- 冪等な挿入では、読み取ってから書き込むのではなく
ON CONFLICT DO NOTHINGを使います。
孤立イベントの処理
存在しなくなったエンティティ(終了したエージェント、閉じたスレッド)に対するイベントが届いた場合は、明示的に処理してください。黙って破棄してはいけません。警告をログに記録し、古い状態をクリーンアップします。孤立イベントでパニックを起こすのは、常に誤りです。
所有権の判断フロー
データへの共有アクセスが必要な場合は、次のフローを順番にたどってください。
- 参照を渡せますか?
&Tを使います。コストも複雑さもありません。 - 所有されている場合と借用されている場合がありますか?
Cow<'_, str>を使います。借用時はゼロコストです。 - 同期境界をまたいで複数の所有者がいますか?
Arc<T>を使います。x.clone()よりArc::clone(&x)を優先します。 - 複数の所有者がいて、変更も必要ですか?
Arc<Mutex<T>>またはArc<RwLock<T>>を使います。読み取りが書き込みを大幅に上回る場合はRwLockを選びます。 - 起動した処理を待たずに実行しますか? 所有データを使う
moveクロージャーを使います。
すべての .clone() は判断の機会です。「このクローンは本当に必要ですか。それとも借用するように構成し直せますか」と問いかけてください。
エラーの方針
| コンテキスト | ツール | 理由 |
|---|---|---|
| アプリケーションコード(main、CLI、テスト) | anyhow::Result + .context() | 詳細なエラーチェーンを得られ、定型コードが不要です |
| ライブラリコード(他のクレートから利用されるクレート) | thiserror の列挙型 | 型付けされ、パターンマッチでき、呼び出し側でバリアントに応じて処理できます |
エラー列挙型の設計:ユーザー向けメッセージでは、内部で何が問題になったかではなく、ユーザーが何をすべきかを伝えます。大きなペイロードは Box<T> にします。is_retryable() では各バリアントを明示的に分類し、ワイルドカードは使いません。
非同期処理に関する判断
Box::pin を使う場合: 薄い非同期ラッパーが大きな呼び出し先の future をステートマシンにインライン化し、スタックに負荷がかかるときです。内部呼び出しをラップします:Box::pin(self.inner_method(args)).await。
| 必要なもの | チャネル | 理由 |
|---|---|---|
| 1つの応答を返す | oneshot | 値を1つだけ受け取り、その後終了します |
| イベントのストリーム | mpsc | 複数の送信側と単一の受信側に対応します |
| 最新の値のみ | watch | 受信側は常に最新の値を確認できます |
| 全員へのブロードキャスト | broadcast | すべての受信側がすべてのメッセージを受け取ります |
シャットダウン:階層的なシャットダウンには CancellationToken を使います。順序が必要な非同期クリーンアップで Drop に依存してはいけません。.await ポイントをまたいで MutexGuard を保持してはいけません。
パス3:簡素化する
コードを追加するのではなく、削除するつもりでレビューしてください。
次の3つの診断用の質問を確認します:
- 呼び出しチェーンを通じて渡されるだけのパラメーターはありませんか? 削除し、アンビエントアクセスを使います。
- 使われていないフィールドや関数はありませんか? 削除します。
- 正当化できない抽象化はありませんか?(一度しか使わないヘルパーや、何も追加しないラッパーなど)インライン化します。
「配線し直すのではなく、書き直す」原則: バグが関数の内部ロジックにある場合は、明示的なチェックを使って本体を書き直します。たまたま現時点で正しい結果を生成する既存APIに委譲してはいけません。明示的に記述したほうが監査しやすく、上流の変更にも耐えられます。
タスクに厳密に必要な量を超えてコードを追加したなら、何かが間違っています。削りましょう。
パス4:検証する
同梱の lint スクリプトをコードに対して実行します:
bash ${SKILL_DIR}/scripts/lint.sh <your-file.rs>
すべての ERROR を修正します。すべての WARNING を確認します。続いて、次の手動チェックリストを実行します:
[ ] Every ? has .context("failed to [verb] [noun]")
[ ] No unwrap() outside #[cfg(test)]
[ ] BTreeMap where output is serialized or compared
[ ] No bool parameters -- use enums
[ ] Match arms exhaustive -- no _ => wildcards
[ ] Config structs derive Default
[ ] No single-use helper functions -- inline if called once
[ ] Module under 500 lines (excluding tests)
[ ] No unnecessary .clone() -- prefer borrowing
[ ] Return structs, not tuples, for multi-value returns
[ ] Cow<str> where allocation is conditional
[ ] serde attrs present: rename_all, default, deny_unknown_fields as needed
[ ] No struct with 2+ Option<T> fields that should be an enum
[ ] No function with 4+ parameters (use args struct)
[ ] Lock guards dropped before any .await
[ ] Events for dead/missing entities handled explicitly
コードを提示する前に、すべての違反を修正してください。
クイックリファレンス:9つの変換
各変換では、初稿から本番品質への正確な差分を示します。
1. すべての ? 演算子に .context() を付ける
変更前:
let content = std::fs::read_to_string(path)?;
let config: Config = toml::from_str(&content)?;
変更後:
let content = std::fs::read_to_string(path)
.context("failed to read config file")?;
let config: Config = toml::from_str(&content)
.context("failed to parse TOML config")?;
パターン:"failed to [verb] [noun]"。上流のエラーはそれ自体を説明します。何の操作が失敗したのかを示すのが、あなたの役目です。
2. ブール値ではなく列挙型を使う
変更前:
fn create_sandbox(network: bool, writable: bool) -> Sandbox {
変更後:
fn create_sandbox(network: NetworkMode, access: AccessLevel) -> Sandbox {
foo(true, false) では意味が伝わりません。列挙型に置き換えると、呼び出し箇所を NetworkMode::Restricted, AccessLevel::ReadOnly のように記述できます。APIを変更できない場合は、意味の分かりにくいリテラルの前に /*param_name*/ コメントを追加します。
3. シリアライズまたは比較するデータには BTreeMap を使う
変更前:HashMap<String, Rule> -- 変更後:BTreeMap<String, Rule>
HashMap のイテレーション順序はランダムなため、差分が不必要に増え、スナップショットテストが不安定になります。データをシリアライズする場合、テストで比較する場合、またはユーザーに表示する場合は BTreeMap を使います。
4. ワイルドカードを使わず、網羅的な match を書く
変更前:
match decision {
PolicyDecision::Allow => true,
_ => false,
}
変更後:
match decision {
PolicyDecision::Allow => true,
PolicyDecision::Block { .. } => false,
PolicyDecision::Rewrite { .. } => false,
PolicyDecision::Ask { .. } => false,
}
新しいバリアントが追加されると、コンパイラーが更新の必要なすべての match を指摘します。ワイルドカードはこれを隠してしまいます。
5. タプルではなく構造体を返す
変更前:fn build_command() -> (Vec<String>, Vec<OwnedFd>) -- 呼び出し側は result.0 と記述します
変更後:fn build_command() -> CommandOutput -- 呼び出し側は output.args、output.preserved_fds と記述します
名前付きフィールドは、それ自体で内容を説明します。2つ以上の値を返す関数には構造体を定義します。
6. 設定用構造体に Default を導出する
#[derive(Default)]
pub struct ServerConfig {
pub timeout_secs: u64,
pub max_retries: u32,
pub bind_addr: String,
}
let config = ServerConfig { timeout_secs: 30, ..Default::default() };
呼び出し側が必要な項目だけを上書きできるよう、Default を導出します。ゼロ以外のデフォルト値が必要な場合は、Default を手動で実装します。
7. 所有権が条件によって変わる場合は Cow<str> を使う
変更前:
fn normalize_path(input: &str) -> String {
if needs_normalization(input) { input.replace('\\', "/") }
else { input.to_string() } // allocates even when unchanged
}
変更後:
fn normalize_path(input: &str) -> Cow<'_, str> {
if needs_normalization(input) { Cow::Owned(input.replace('\\', "/")) }
else { Cow::Borrowed(input) } // zero-cost when unchanged
}
一部のパスでのみ割り当てが必要な場合、Cow<str> を使うと不要な無条件の割り当てを避けられます。
8. 列挙型による非交和
変更前:
struct Permissions {
profile_name: Option<String>, // only for named profiles
sandbox_policy: Option<SandboxPolicy>, // only for legacy
file_paths: Option<Vec<PathBuf>>, // only when sandbox_policy is set
}
変更後:
enum Permissions {
Named { profile_name: String },
Legacy { sandbox_policy: SandboxPolicy, file_paths: Vec<PathBuf> },
}
構造体に、特定の組み合わせでのみ有効な Option<T> フィールドがあると、型レベルで無効な状態を許容してしまいます。各バリアントに関連するデータだけを持たせる列挙型に変換します。診断の目安:is_some()/is_none() ガードを使う Option<T> フィールドが2つ以上ある場合は、列挙型にするべきです。
9. 長いパラメーターリストを構造体にする
変更前:
fn spawn_process(
cmd: &str,
args: &[String],
env: &BTreeMap<String, String>,
cwd: &Path,
stdin_policy: StdinPolicy,
sandbox: SandboxPolicy,
timeout: Duration,
) -> Result<Child> {
変更後:
struct SpawnArgs<'a> {
cmd: &'a str,
args: &'a [String],
env: &'a BTreeMap<String, String>,
cwd: &'a Path,
stdin_policy: StdinPolicy,
sandbox: SandboxPolicy,
timeout: Duration,
}
fn spawn_process(args: &SpawnArgs<'_>) -> Result<Child> {
パラメーターが4つ以上になると、呼び出し箇所が読みづらくなり、順序を間違えやすくなります。引数用の構造体を使うと、呼び出し箇所で各フィールドに名前を付けられ、将来パラメーターを追加しても互換性を損ないません。