検索/システム系のコードを、AIに型とエラー処理まで丁寧に書かせる

システム系のコードを、AIに型とエラー処理まで丁寧に書かせる

厳しい品質が求められるシステム系のコードでAIに手伝わせる時に使う。書いた時間の3倍以上かけて見直し、すべてのエラーの場合を扱い、決まりを型で表し、古い保存形式とも合うようにし、どれもビルドが通る小さなコミットに分ける。

このAIルールを使うには
## AIが書いたコードを、書いた時間の3倍以上かけて見直す アシスタントは速度の増幅器ではなく、品質の増幅器として扱う。それによって浮いた時間は、支援なしの作業では届かない水準の厳密さに充てる。 - 卓越を目指す:より多くのエッジケースを網羅し、理解しやすさのためにリファクタリングし、残っているTODOを片付け、面倒な作業をこなし、バグゼロを目標にする。 - 生成にかけた時間よりはるかに長くレビューする。生成されたコードのレビューには、それを生み出すのにかかった時間の少なくとも3倍を費やし、1行ごと・設計判断ごとに吟味し、壊す方法を能動的に探す。 - 成果物の責任は作者にある。方向性についての意見だけを求めていると明示している場合を除き、初稿をそのまま完成した仕事として渡さない。 - 注意深い人間のレビューなら気づいたはずのケースが抜けているなど、丁寧さを欠いて作られた形跡のある成果物は、直させるのではなくその場で受け取りを断ることがある。 - コード変更を伴う会話の冒頭と、プルリクエストを出す前に、この基準を宣言し、作者とレビュアーが同じ水準を共有する。 ## 正しさを型に持たせ、すべてのエッジケースを扱う 便利な実装より正しい実装を選び、可能な限り多くの正しさを型システムへ押し込んで、間違いが本番で失敗するのではなくコンパイルで失敗するようにする。 - エラー空間の全体をモデル化する。近道も、簡略化したエラー処理もしない。 - 競合状態、シグナルのタイミング、オペレーティングシステム間の挙動の違いを含め、あらゆるエッジケースを扱う。 - 正しさの制約を型で表現する:ドメイン値のラッパー型、複雑な構築のためのビルダーパターン、状態遷移が重要な場合はジェネリクスでの型状態、そしてクローンではなく借用。 - 選べる場面では常に、実行時チェックよりコンパイル時の保証を選ぶ。 - 可視性はできる限り狭く保つ。モジュールの外にある何かが本当に必要とする時にだけ、その項目を広く公開する。 - 安定していて外部から利用されるパッケージの公開型には、前方互換性のために拡張可能である印を付ける。安定性を約束しない内部パッケージにはこれは不要だが、エラー型には付けたい場合がある。 - 共有された可変状態の代わりにメッセージパッシングまたはアクターモデルを使い、データ競合が起こり得ないようにする。 - 細部を正しくすることは、早く終わらせることより重要である。 ## 利用者に役立つエラー表示を出し、どのOSでも同じ動きにする ユーザーに見える挙動は、仕上げの装飾ではなく設計の入力である。 - 素のエラー文字列ではなく、構造化された役立つ診断を出し、メッセージが人の目に触れる場所では常にリッチな診断ライブラリを使う。 - 進捗の報告は、応答性が高く情報量のあるものにする。 - 基盤となる機能が異なる場合でもプラットフォーム間で挙動を一貫させ、あるプラットフォームを別のプラットフォーム上でエミュレートするのではなく、各プラットフォームをそれぞれのネイティブな仕組みで実装する。 - ユーザーに見えるメッセージは明快な現在形で書く:"the tool now supports" であって "the tool now supported" ではない。 - 複数形の処理は共有のヘルパーモジュールに集約し、その関数が個数に応じた単数形または複数形を返すようにする。新しいヘルパーは、呼び出し側に複数形の処理ロジックを直接書くのではなく、そのモジュールに追加する。 ## 設計を少しずつ育て、選んだ理由を設計書に残す 設計は抽象的で過度に汎用にせず、具体的で組み合わせ可能に保つ。 - 最初から完璧なアーキテクチャを目指すのではなく、設計を漸進的に発展させる。 - 設計上の判断とそのトレードオフを、プロジェクトと共に保管される設計文書に記録し、後の読み手がなぜその形が選ばれたのか分かるようにする。 - 確信が持てない時は、推測を抽象化として固めてしまうのではなく、探索して反復する。 ## 利用者が原因のエラーと内部のバグを分け、詳しい情報を付ける エラーはユーザーが目にする表層の一部であり、成功する経路と同じだけの設計上の注意に値する。 - 文字列型のエラーではなく derive ベースのエラー型を使い、種類が増えてきたら関連するエラーを kind の列挙型の下にまとめる。 - 詳細を単一のメッセージに潰し込むのではなく、エラーに豊かで構造化された文脈を持たせる。 - 二層のモデルを使う。ユーザー起因のエラーと外部起因のエラーは、意味づけされた終了コードに対応する独立した型とし、内部のプログラミングエラーはそれとは別で、パニックしてよい。 - エラーの表示テキストは小文字の文の断片として書き、"failed to ..." のような包み込む語句に埋め込んだ時に正しく読めるようにする。 ## 設定ファイルの知らない項目を警告し、設定エラーを分かりやすくする デシリアライズの判断が、ユーザーが今後目にするすべての設定エラーの品質を決める。 - 設定の中で無視されたパス・未知のパスを追跡して報告し、打ち間違いが黙って何もしないのではなく警告として表に出るようにする。 - デシリアライズの最中に入れ子の構造を平坦化することは決してしない。それに必要な内部バッファリングが未知フィールドの警告を劣化させる。代わりに、フィールドを明示的に構造体へ写す。 - タグなしのバリアントのデシリアライズには決して頼らない。エラーメッセージが貧弱になるからである。代わりに、正確な期待値の文字列を持つカスタムのビジターを書く。 ## 保存や通信の形式を変える前に、古い版と新しい版で読み書きできるか確かめる ディスクに書かれる、あるいは通信で送られる構造への変更はすべて、取り込まれる前に互換性のマトリクス全体を通して追跡しなければならない。 - 古い読み手と新しいデータ:それでもデシリアライズできるか、そして情報を失わないか。 - 新しい読み手と古いデータ:宣言された既定値は、単に妥当なだけでなく正しい値を生むか。 - 古い書き手と新しいデータ:フィールドを落とさずに往復できるか。これが最も見落とされやすいケースである。 - 形式のバージョンは先回りして上げる。新しいフィールドが意味的に重要になるなら、既定値以外の値が初めて書かれる時ではなく、そのフィールドを追加した時点でバージョンを上げる。そうしないと、古いビルドが書き戻す際に黙ってデータを壊す。 - 既定値は必要だが十分ではないものとして扱う。既定値は古い読み手が新しいデータを解釈できるようにするが、古い書き手は依然として自分の知らないフィールドを落としてしまう。 ## 非同期は必要な所だけで使い、仕事を際限なく起動しない 非同期は入出力と並行性のための道具であって、既定の書き方ではない。 - マルチスレッドの非同期ランタイムを使うが、非同期はそれを必要とするコードに閉じ込め、それ以外はすべて同期のままにする。 - 構造化並行性を使い、並行に走る仕事に不自然に長い生存期間を与えずに済むようにする。 - 無制限に spawn するのではなく、バックプレッシャーを意識してキューに入った仕事をスケジュールする。 - ジョブ制御がプロセスを一時停止・再開できる場面では、一時停止していた時間がタイムアウトに算入されないよう、一時停止できるタイミング用のプリミティブを用意する。 ## 集約用のモジュールにロジックを置かず、インポートは先頭に書く モジュールの境界は、慣習だけではなく、構造と可視性によって強制される。 - 集約用のモジュールファイルは、公開項目の再エクスポートにだけ使う。自明でないロジックは、実装用のサブモジュール、あるいはもっと具体的なモジュールに属する。 - プラットフォーム固有のコードは、条件付きコンパイルの後ろでプラットフォームごとの別ファイルに置き、それらの間に共有のインターフェースを設ける。 - テスト用のヘルパーは、専用のモジュールまたはファイルに置く。 - 型と関数はモジュールの最上部でインポートする。関数の本体の中でインポートすることは決してしない。唯一の例外は、ゲートされたインポートを必要とする条件付きコンパイルの関数である。 - 型そのものをインポートすることを優先し、モジュールをインポートするのは、それが慣例的な書き方である場合だけにする。シグネチャでの完全修飾パスは避け、親モジュールをインポートしてそれだけで修飾する。 - パターンマッチのために列挙型のバリアントをインポートするのは問題ない。 ## データはコピーせず、借用や共有で使い回す 後から最適化するのではなく、そもそも仕事が発生しない表現を選ぶ。 - 不変のデータは、コピーするのではなく参照カウントまたは借用によって共有する。 - 短い文字列を多く扱うコードでは、小さな文字列に最適化された型を使う。 - クローンと参照に注意を払う。借用がすでに表現している自然な木構造や所有構造をデータが持っている場合は、クローンを避ける。 - 可能な場所では、データをすべてメモリに溜め込むのではなく、イテレータを通して流す。 ## コミットの前に整形をかけ、依存関係のバージョンを1か所で管理する 機械的な一貫性は、強制するのは安上がりだが、後から適用し直すのは高くつく。 - すべてのソースファイルは、プロジェクトの著作権表示の行とライセンス識別子から書き始める。 - 言語のエディションを固定し、コミットのたびにその前にプロジェクトの整形コマンドを実行する。整形は継続的インテグレーションで強制されるので、整形されていないコミットはそこで失敗する。 - lint を抑制する時は、許可する形ではなく期待する形を使い、その lint がもう発火しなくなった時に抑制そのものが警告を出し、効かなくなった抑制が溜まらないようにする。 - 整形済みのテキストを文字列に追記することを lint が禁じている場所では、整形した文字列を push する代わりにプロジェクトの書き込み用マクロを使う。 - 依存関係のバージョンはすべてワークスペースのルートで一元管理し、内部パッケージは厳密なバージョンに固定する。 - 自明でない依存関係や機能フラグの選択には、特にセキュリティやサイズを理由に機能を無効にしている場所には、コメントを付ける。 ## コメントには、何をするかではなく、なぜそうするかを書く - コメントは、何をしているかではなく、なぜそうするのかを説明する。 - 関数の本体の中で実況をしない。自明でない点や特別な点がある場所、あるいは意図の踏み込んだ説明が必要な場所にだけコメントを付ける。 - モジュールレベルのドキュメントには、そのモジュールの目的と責務を書く。 - コードのコメントは必ずピリオドで終える。 - 見出しやタイトルでタイトルケースを使うことは決してしない。常にセンテンスケースを使う。 - 常にオックスフォードコンマを使う。 - 冠詞を省略しない。"file has newer version" ではなく "the file has a newer version" と書く。 ## プロジェクト指定のテストランナーでテストを動かし、今あるフィクスチャを使う プロジェクトがテストを走らせるやり方でテストし、すでに存在するテストのモデルを再利用する。 - 単体テストと結合テストには、プロジェクトが指定したテストランナーを使う。テスト群がその指定ランナーの実行モデルに依存している場合、言語の既定のランナーに引き返さない。 - ローカルでビルドしたツールのコピーを使うのは、新しいフラグや出力形式など、ツール自身の挙動の変更を検証する時だけにする。テスト群そのものは標準のランナーで走らせる。結合テストはビルド成果物から自分の内側のプロセスを起動するからである。 - メインのランナーがドキュメンテーションテストに対応していない場合は、それに対応したコマンドで走らせる。 - 単体テストはそれがテストするコードの隣に、結合テストは専用のパッケージに、共有のフィクスチャはフィクスチャ用のパッケージに、テストのユーティリティはサポート用のパッケージに置く。 - 手書きの部分的なチェックより、フィクスチャ用パッケージが持つ期待結果のモデルを優先する。 - すでに存在する道具立てに手を伸ばす:パラメータ化したケース、プロパティベーステスト、スナップショットのアサーション、カスタムのハーネス、そして改善されたアサーション出力。新しい仕組みを足す前に、何が使えるか確認する。 ## コミットの件名に範囲を付け、どのコミットもビルドが通るようにする 履歴はレビュー可能な成果物であり、各コミットをレビューの単位として扱う。 - 各コミットの件名は、触れた範囲を角括弧で囲んだ接頭辞から始め、その後に短い説明を続ける。ツールチェーンの最低要件、リリース、継続的インテグレーションの変更のような横断的な作業には、メタ用の範囲を使う。 - バージョンを上げるコミットとリリース準備のコミットは、それぞれ固有の決まった件名に従い、通常はリリースツールが生成する。 - 本文は単純過去形と現在形で書く:以前は何が起きていたか、今は何が起きるか、そしてどのテストが追加されたか。 - コミットメッセージはMarkdownである。件名の行ではバッククォートを使わないが、本文では使う。 - コミットは原子的にする。1つのコミットは1つの論理的な変更単位である。 - 履歴は二分探索可能に保つ。すべてのコミットがビルドでき、すべてのチェックを通らなければならない。 - 関心事を分ける。整形の修正とリファクタリングは、挙動の変更とは切り離して、それぞれ独自のコミットに属する。 ## 並行ランナーを、ディスパッチャとエグゼキュータに分けてメッセージでつなぐ 並行に動くランナーは、一方のコンポーネントが外の世界を担い、もう一方が実行を担う時に、最も理解しやすくなる。 - ディスパッチャは外の世界とやり取りし、あらゆるソースからのイベントを直列化し、select 構文でそれらのソースを多重化し、今何が動いているかの正となる状態を保持する。同期点が、報告を競合から自由に保つ。 - エグゼキュータは仕事の単位を非同期の状態機械としてスケジュールして実行し、再試行、タイムアウト、プロセスの起動、出力の取り込みを扱う。各単位は自分自身の状態についての真実の源であり、ブロードキャストではなく、ディスパッチャへの専用のチャネルを持つ。 - 状態を直接共有しない。すべてはメッセージとしてコンポーネント間を渡る。 - 失敗の空間の全体をモデル化し、一時停止と再開のシグナルがタイミングを歪めないよう、一時停止できるタイマーを使う。 - 各プラットフォームのネイティブなプロセス封じ込めとシグナルの機能を使い、条件付きコンパイルと、共有インターフェースの後ろに置いたプラットフォーム別のモジュールでそれらを守り、プラットフォームの差異とトレードオフをコメントに記録する。