RustでWIPコードを警告として残す
(blog.dureuill.net)- Rust開発では、すべてのエラーパスや所有権の問題をすぐに整理しようとすると実装の流れが途切れやすく、まず happy path を作って残りの作業を 警告 として追跡する戦略が有用
- 標準のRustだけでも
unwrap、clone、todo!、標準のコンパイラ警告、///ドキュメントコメントのトリックを組み合わせて未完成コードを示せる - 従来の方法では、
unwrapとcloneは警告を出さず、todo!も間接的な警告に依存し、///トリックは場所によってはコンパイルエラーや意図しないドキュメント化につながることがある wipcrate はwip!、unwrap_wip、clone_wip、fixme!により、一時的な実装、一時的なunwrap/clone、後で直すためのメモを 常に警告 として可視化する- Meilisearch は CI で
-D warningにより警告をエラーへ変えてWIPコードの流入を防ぎ、開発中に追加したwip依存はレビュー前の履歴整理の過程で削除する運用をしている
Rust開発中に警告が必要な理由
- Rustは 正確性 を重視する言語だが、開発途中ですべてのエラーパスをその場で処理すると、実装速度や集中力が落ちることがある
rustcがResultの返却、エラー処理、エッジケース実装などを理由にコンパイルを止めると、通常は次の二択になる- すぐに完全なエラー処理を実装し、PRの準備前までに何度もリファクタリングする
TODO later的な印を残して問題を 警告 レベルまで下げ、いまの作業を続ける
- 警告は開発を止めずに、PRマージ前に整理すべきサインとして残せる
- Meilisearch では CI でコンパイラを
-D warningフラグ付きで実行し、すべての警告をエラーに変えることで、WIPコードが誤って配布されないようにしている - コードレビューはバグを防ぎ、チーム内で知識を共有するための基本的な慣行だが、人の記憶や注意力だけに依存すると結局ミスは起きるため、追加のガードレール が必要になる
標準Rustで未完成処理を後回しにする方法
-
unwrapでエラー処理を後回しにするResultやOptionにunwrapを使えば、エラーパスでは panic する代わりに中の値を取り出して実装を先へ進められる- 残りのコードができたあとで、一時的に追加した
unwrapを選んで削除し、適切なエラー処理に置き換えられる - この方法は、Rustがエラーを正しく扱うための構造を提供していても、その処理を ただちに 完了する必要はないという前提に基づいている
-
cloneで所有権の問題を後回しにする- 所有権の問題はエラー処理より頻度は低いが、正しい所有権の維持に設計変更が必要になることがある
- その場合、値をムーブする代わりに
cloneしてライフタイムの問題を一時的に回避できる - 後で適切な変数をより上位のスタックへ渡す形にして clone を取り除けるケースを、経験的に見分けられることがある
- ただし修正がコード構造の変更につながることもあり、エラー処理より後回しにしづらい
-
todo!()で実装の場所だけ確保する- Rust標準ライブラリの
todo!()マクロは未完成コードを表し、プロトタイピング中に型解析を通すためのプレースホルダーとして便利 todo!()は panic によって diverge するため、多くの型の位置でプレースホルダー式として使える- インターフェースを固めている最中に関数定義を使えるようにしつつ、実際の実装は後ろへ回せる
todo!()はプログラム利用者に未実装機能を知らせるというより、コード作成者やレビュー担当者のための WIP向けの便宜機能 に近い- 利用者に機能未実装を知らせる用途には
unimplemented!()マクロが別にある
- Rust標準ライブラリの
-
標準コンパイラ警告とドキュメントコメントのトリック
- Rustコンパイラの標準警告も、開発中の未完成状態を示すのに役立つ
- 未使用の束縛
- 不要な
mut - 未使用の
ResultとOption
- 開発中にこうした警告を
_接頭辞や別のトリックで 一時的に消す と、未完成コードがそのまま通ってしまう危険がある - 型システムでは追跡されないエッジケースや壊れたロジックは、コメントとして残すことがある
///のようなドキュメントコメント風に書くと、Rust は文書化対象のないドキュメントコメントに対して警告を出すため、その位置の問題を警告として追跡できる
- Rustコンパイラの標準警告も、開発中の未完成状態を示すのに役立つ
標準方式の限界
- 最大の問題は、どの手法も解決すべき問題に結び付いた警告を安定して作れないこと
todo!()はしばしば警告を生むが、関数引数が使われていない、必要な mutation が起きていないといった 間接的な原因 から出ることが多いunwrapとcloneはまったく警告を出さないため、最終PRの前に読み直して削除する必要があるunwrapは本番コードにそのまま残ることもあり、一時的なWIP用途と通常用途が混ざってしまう///ドキュメントコメントのトリックはハックに近く、式の位置ではドキュメントコメントが禁止されているためコンパイルエラーになることがある- 実際にドキュメント化される位置で
///を使うと期待した警告が出ず、配布コードに不要なドキュメントが残ることもある
wip crate が提供するツール
wipcrate は、Rust開発中の「後で処理する警告」戦略をより使いやすくするためのツール群- 通常の crate と同じように追加できる
cargo add wip
- 必要なファイルでは prelude を取り込める
use wip::prelude::*;
wipは Rust の deprecation warning に依存するため、エディタが deprecated 関数呼び出しに打ち消し線を引く機能はオフにしたほうが煩わしくない場合がある-
wip::wip!wip!マクロはtodo!のように動作するが、使うたびに必ず 警告 を出す- 未完成コードを示すために
todo!を使っていた場所に置ける - 初期バージョンでは
todoという名前を使っていたが、依存関係がstdマクロを shadow しようとすると Rust がそれを好まないため、prelude 利用時に問題があった
-
unwrap_wipとclone_wipunwrap_wipとclone_wipは、ResultとOptionに対する extension trait として実装されている- スコープ内にあれば通常の
unwrap、cloneのように動作するが、使用時に警告を出す - 最終コードに
unwrapやcloneを残す意図ではないことを明確に示せる
-
wip::fixme!fixme!マクロはwip!の non-panicking 版で、式の代わりにはならない- TODOコメントのように使えるが、
///トリックなしで安定して警告を生成する - 後で直すべきメモを残す用途でよく使われる
利用フローと注意点
wipcrate は最近いくつかのPRで使われており、大きなPRのひとつである Meilisearch PR #6484 でも使われている- 一般的な流れとしては、開発中にワークスペースの crate へ
wip依存を追加し、レビュー前に履歴を書き換える過程で削除する wipは happy path に集中しながら、後で処理すべき作業を忘れないようにしてくれる- 欠点もある
fixmeを追加するときに十分な文脈を書かないと、あとで直しにくい- 大きなPRでは
wipが作る警告の数が多くなり、負担になることがある
- 現在のAPIは docs.rs の
wipドキュメント で確認できる
1件のコメント
Lobste.rs のコメント
正直、
wipの理屈を見ると、本来ツールをどう使うべきか理解しないまま Clippy を再発明したように見える。ツールチェーン開発者たちは何を
rustcの警告にし、何を Clippy lint にするかを分けてきたのに、wipは既存機能を別名で作り直し、cargo clippyにあった lint をcargo checkやcargo build側へ無理やり持ち込んでいるように思える。たとえば
wip!の説明は、新しい依存関係と非標準構文を追加する代わりに#![warn(clippy::todo)]をcargo clippyの外でも出せるようにするという話で、unwrap_wipも#![warn(clippy::unwrap_used)]に近い。私にとって
.unwrap()はプロトタイピング用なので#![warn(clippy::unwrap_used)]は有効にするが、.expect("description of invariant")は.unwrap_wip()の代わりに.unwrap()を使いそうな本番コード向けなので#![warn(clippy::expect_used)]は有効にしていない。例:.expect("parse regex from const")clone_wipとfixme!についてはよく分からないし、そういうケースを開発者がどう扱う想定なのか答えられるほど考えたこともない。ただ、既存の linter である Clippy に関する議論が記事で抜けているのはその通り。これは個人的な使い方にも関係していて、以前は保存時に Clippy を回していたが、うちのプロジェクトでは遅すぎて保存のたびに数分かかるようになり、やめてしまった。今は CI と push 前フックでしか回していない。
警告を
rustcに持って来ればずっと速くなるし、反復作業中にインライン警告が見えるので、このワークフローの重要な部分になっている。Clippy の設定も、少なくとも最後に確認した時点ではかなり面倒だった。ワークスペース内の全クレートに lint をコードで一括指定する方法がなく、ワークスペースで拒否したい lint ごとに CI に明示的な
-Dを追加するか、すべてのクレートにグローバル属性を書き続ける必要があった。#![warn(clippy::todo)]はすべてのクレートに必要なので、開発中だけクレートを追加・削除するより私たちには大きな利点ではない。todo!はデフォルトでrustcか Clippy が警告すべきだと思うし、そうなればwipの有用性が下がるという点には同意する。unwrap_wipが#![warn(clippy::unwrap_used)]だという話には、二つの意味で同意しない。第一に、意図の面でunwrapは非本番、expectは本番という区別はよくあるが普遍的ではない。第二に、既存コードベースには約 4600 個の unwrap があり、全部をexpectに置き換えることはできるとしても、そのためにあちこちへ暫定メッセージを入れたり、「この mutex は poison されていない」「この失敗を呼び出し元へ伝播させたい」といった前提条件を組み立て直したりするのに時間を使いすぎる。fixme!はとても有用で、標準ライブラリなどに入ってほしい。実際、このクレートでいちばんよく使っている機能だ。記事ではこうした細部を扱っていないが、このクレートは標準的なものではなく特化ライブラリなので、
wip_iterのような機能もある。イテレータを返す位置のimpl Trait関数でtodoのようなマクロを使えるようにしてくれるし、wip_futureも同様だ。返り値型推論とtodoがうまく一緒に動かず、無理やり型を作らなければならなかったことがかなりあった。リリースモードではデフォルトでコンパイルエラーにしようという提案もあったし、ほかの改善案と合わせて検討する予定だ。新しい依存関係と非標準構文というコストについて少しバランスを取って言えば、これは開発中にだけ使い、終わる頃には普通は削除する単一ファイル依存だ。
unwrapでプロトタイプを書くときの問題は、あとで正しいエラー処理を導入しようとすると 戻り値型の形 が変わり得て、とくにそれらの関数を関数型スタイルで使っていると連鎖的な影響が出ることだ。エラー型は
Box<dyn Error>、文字列、anyhowなどを使って、最初からResultにしておくのを勧める。あとで本物のエラー列挙型に変えることはできるが、失敗しないように見えるコードを後からResultを返すようにリファクタリングする方が難しい。また LLM を使うなら注意が必要だ。LLM はパターンマッチング機械なので、「意図的に雑にしていて後で直したい」と「このプロジェクトではこうエラー処理すべきだ」を区別できない。時間がたつと「とりあえず今だけ」の判断を増殖させかねないので、main にコメントを一つ二つ入れて、どこで/いつ緩いエラー処理が許されるのか案内しておくと役に立つ。
todo!()とunimplemented!()の解釈については、私とドキュメントで見解が一致していない。ただし、省略された次の段落がその立場をある程度バランスよく補ってはいる。ドキュメントでは、
todo!は後で実装する意思を伝え、メッセージは “not yet implemented” である一方、unimplemented!はそうした約束を含まず、メッセージは “not implemented” だとされている。todo!が 1.40.0 で追加されたとき は、単に名前が短いだけで、名前以外はunimplemented!と違いがなかった。1.42.0 になって初めて
unimplementedのメッセージから “yet” が外れた。記憶では、両マクロにこうした意味づけをすること自体がやや議論のある話で、todo!をunimplemented!の短縮表記として出したことも同様だった。最近では
unimplemented!()は、技術的な理由でメソッド本体を用意しなければならないが、決して呼ばれてはならないトレイト実装のような場所で推奨されている。ドキュメントの例 もまさにそういう形だ。開発中にはワークスペースのクレートに
wip依存を追加し、レビュー前に履歴を書き換えて削除する、という流れは、追加ツールでもう少し滑らかにできるのではないかと思う。PR の途中を修正しながら ロックファイルの書き換え まで必要になるのは少し気が進まない。その間に依存関係ツリーが別の場所でも変わっていたら、不要なコンフリクトが生じるかもしれない。
jj fixを乱用できるのかもしれない。いずれにせよ、プロジェクトのアイデア自体はとても良くて、どこかで使うことになりそうだ。wip::fixme!のような、panic しないメモ用マクロは本当に良いアイデアだ。ドキュメントコメントのトリック も覚えておきたい、すっきりしたやり方だ。rustc側の変更につながるとよさそうな面白いアイデアがある。todo!()には本当に デフォルト警告 が出るべきだ。開発者がハックモードに入っているときには、こうした警告を消せるとさらに良い。警告が多すぎると圧倒されるし、たいてい IDE の下にある 19 行のコンソールで
cargoを回している。開発中の警告が、実際のエラーやもっと重要な警告を埋もれさせてしまう。常にエラーを最後に並べる規律があるだけでも、この問題は和らぐはずだ。それに
.unwrap_wip()と.clone_wip()という名前は元の名前より長く、長すぎる。「dev unwrap」と「dev clone」の略として.du()と.dc()のような名前を勧めたい。そうすれば高速なプログラミングでより使いやすくなる。必要な場所に
.clone()と.unwrap()を 自動で追加 できないだろうか。たとえば#[wip(autoclone,autounwrap)]のようなフラグや機能で。たぶん無理だろうが、夢を見ることはできる。新しいプロジェクトやプロトタイプを始めるたびに、
main.rsに次の行を入れている:#![allow(dead_code)]#![allow(unused_variables)]#![allow(unreachable_code)]初期段階では多くのノイズを減らしてくれる。