考えすぎ、スコープ拡大、構造的 diff でプロジェクトを台無しにする方法
(kevinlynagh.com)- プロジェクトは すぐ作って終わらせる流れ と、調査や設計が膨らんで本来の問題を見失う流れに分かれやすく、実際の前進では とにかくやってみる側 のほうが先に進むことが多い
- キッチン棚は週末のうちに完成した一方で、構造的 diff ワークフローの探索や長く引きずってきた言語・CAD 構想は、数多くの調査やプロトタイプにもかかわらず、最初の動機を直接解決する成果物には結び付かなかった
- Emacs 向けの fuzzy path 検索を作る際にも、良いライブラリの付加機能 が新たな要件を生み、設計が肥大化した末に、不要だったアンカー機能のコードをすべて捨てることで YAGNI を再確認することになった
- コード diff では行単位比較が関数や型のような上位構造を適切に捉えられず、treesitter ベースのツールでもエンティティ対応付けがずれると、削除と追加が長々と表示されて読みにくくなることがある
- 必要なのは、LLM 出力のターンごとのレビュー に合った最小限のツールを先に作ることであり、Rust 向けのエンティティ抽出と単純なマッチングから始めて、高レベルな変更概要 を素早く見るワークフローを優先すべきである
考えすぎとスコープ拡大
- プロジェクトは すぐ作って終わらせる流れ と、先行事例を掘り下げるうちにスコープが広がり、肝心の元の問題を解決できなくなる流れに分かれやすい
- 週末に作ったキッチン棚は、コーヒーを飲みながら設計を決め、3D プリントのハンガーを何度か修正したあと、余っていた資材と塗料を使って週末のうちに完成した
- Ikea bin ハンガー用 CAD は OnShape CAD で公開されている
- 資材は 作業台 で余っていたものを再利用し、角は palm sander で目分量に整えた
- この棚では、キッチンにぴったり合う物を作ることよりも、友人と一緒に木工を楽しむことが 主な成功基準 であり、そのおかげで細かな基準を過剰に悩まずに済んだ
- 一方で構造的 diff ツールを探す過程では、difftastic の結果に不満があり、関連ツールやワークフローを 4 時間かけて調べたものの、結局は Emacs で使えるより良い diff ワークフローという元の基準に立ち返ることになった
- ハードウェア試作インターフェース、Clojure と Rust を混ぜた言語、CAD 向け言語といった長年の関心事には、背景調査や小さなプロトタイプに何百時間も費やしてきたが、最初の動機を直接解決する成果物にはまだつながっていない
- ハードウェア試作インターフェースは 2023年9月、言語設計は 2023年11月、CAD 関連のアイデアは constraints、bidirectional editing、other dubious ideas へと続いている
- 言語と CAD のプロジェクトでは、Rust や Clojure を置き換えるのか、一部の問題だけを扱うのか、学習用の遊び場で十分なのか、商用 CAD を置き換えるのか、他人にも有用であるべきなのかといった 成功基準が曖昧 になっている
- こうした問いを検討すること自体には価値があるが、多くを検討だけするより、実際にたくさん作ってみるほうがよいと考えている
- 後から見れば明らかに良くない成果物になるとしても、とにかくやってみる側 のほうが全体としては前に進める
スコープ拡大の保存則
- むやみに作る時間にも限界はあり、バランスが必要で、LLM agent で大量にコードを書いた末に結局すべて捨てた経験から、あらためて YAGNI を思い出した
- Emacs で使える Finda スタイルのファイルシステム全体に対する fuzzy path 検索を作りたくて、以前に同じ機能を手書きしたこともあり、LLM を監督すれば数時間で終えられると見ていた
- 最初は計画用の対話で Nucleo を勧められ、設計もドキュメントも良かったので、smart case と Unicode normalization 機能を得るため採用した
- たとえばクエリ
fooはFooとfooの両方にマッチするが、Fooはfooにはマッチしない cafeとcaféの扱いも同じ文脈で処理される
- たとえばクエリ
- 問題は良いライブラリそのものではなく、Nucleo がアンカー機能までサポートしていた点だった
- ファイルパスだけのコーパスでは行頭アンカーは役に立たないように見えたため、これを パスセグメント基準のアンカー として解釈しようとした
- たとえば
^fooは/root/foobar/にはマッチするが/root/barfoo/にはマッチしないようにしたかった
- たとえば
- これを効率よく処理するには、インデックスがセグメント境界を保持し、各セグメントについてクエリを高速に検査できる必要があった
- さらに
^foo/barのようにスラッシュを含むアンカークエリも扱う必要があり、セグメント単位の検査だけでは/root/foo/bar/baz/のようなパスを正しくマッチさせにくくなった - この設計にさらに数時間を費やし、LLM とアイデアをやり取りし、Nucleo の型をラップするコードまで書いたが、コードが大きくなりすぎて気に入らず、結局はもっと小さなラッパーを自分で書き直した
- 休憩したあと、Finda でアンカー機能が必要だった場面を思い出せないこと、パスのコーパスではクエリの前後に
/を付ければたいていのアンカー用途を代替できることに気付いた- 例外として残るのはファイル名末尾に対するアンカーだけだった
- 結局アンカー関連のコードはすべて捨ててしまい、LLM や他人との議論を挟まずに最初から自分で書いていた場合より、なお得だったのかどうかも確信が持てない
- プログラミング速度が上がるほど、それに応じて 不要な機能、rabbit hole、回り道 も一緒に増えるという、ある種の保存則があるように思える
構造的 diffing
- コードにおける diff は通常、ファイルの 2 つのバージョン間の 行単位の変更要約 を指し、unified view では追加と削除を
+、-で示す - 同じ diff は左右比較の形でもレンダリングでき、変更が複雑になるほどこちらのほうが読みやすい場合がある
- 行単位 diff の問題は、関数や型のような上位構造を認識できない点であり、波かっこがたまたまつじつまが合ってしまうと、別の関数に属している場合でも表示が省略されることがある
- difftastic は treesitter が提供する concrete syntax tree を使ってこうした問題を減らそうとしているが、バージョン間のエンティティ対応付けが常にうまくいくわけではない
- 直接のきっかけになった diff では、
struct PendingClickが左右で対応付けられず、左側では削除、右側では追加として表示された - なぜマッチングに失敗したのかまでは掘り下げなかったが、diff 全体が長くなったとしても、
PendingClickRequestとPendingClickが左右で対応しているように見えるほうが良いと判断した
構造的 diff ツールと参考資料
- 最も完成度が高く、慎重に磨き込まれた semantic diff ツールとしては semanticdiff.com を挙げている
- ドイツの小さな会社が提供しており、無料の VSCode プラグインと、GitHub PR diff を表示する Web アプリ がある
- ただし、望んでいるワークフローの土台にできるコードライブラリは提供していない
- semanticdiff vs. difftastic の記事には有用な詳細が多く、difftastic が Python で 意味のあるインデント変化 すら表示できない問題も含まれている
- 著者のひとりは HN コメント で、treesitter を意味処理に使う方針から離れたと述べており、文脈依存キーワードや lexer の挙動のためにパースが失敗し、
asyncのような名前をパラメータに使った場合ですらツールが停止しうると書いている
- diffsitter は treesitter ベースで、MCP server を含む
- GitHub star 数は多いが、ドキュメントはそれほど充実しているように見えず、動作方式を説明した資料も見つけにくかった
- difftastic の wiki には、ツリーの leaf に対して longest-common-subsequence を実行すると書かれている
- gumtree は 2014 年の研究・学術的背景から生まれたツールである
- Java が必要なため、Emacs ですぐ使いたいという個人的用途には合わない
- mergiraf は Rust で書かれた treesitter ベースの merge-driver である
- architecture overview がよく整理されており、内部では Gumtree アルゴリズムを使っている
- ドキュメントや図を見ると、細やかに書かれたプロジェクトという印象を受ける
- semanticdiff.com の著者は HN コメント で、GumTree は結果を素早く出すものの、複数の後続論文による改良を適用しても、かなり頻繁に悪いマッチングを返し、最終的にはマッピングコストを最小化する dijkstra ベースのアプローチ に移行したと書いている
- weave は Rust で書かれた別の treesitter ベース merge-driver である
- 派手なランディングページ、多数の GitHub star、MCP server など、全体的な印象はやや大げさに見える
- エンティティ抽出クレート sem を確認した
- 中核の diff コードは悪くないがやや冗長で、エンティティのマッチングには greedy アルゴリズム を使っている
- データモデルはファイル内移動を検出できず、そのような移動は意味的に大きい場合がある
- 信頼するにはより強い言語統合が必要に見える、ヒューリスティックベースの impact 分析 も多く含まれている
sem diff --verbose HEAD~4を実行したとき、実際には変わっていない行が変更されたと表示されるバグ出力にも遭遇した
- 80% ほど完成した仮想的に便利な機能が多すぎて基盤として使うには合わなかったが、3 か月でここまで作った点は高く評価している
- diffast は 2008 年の学術論文のアルゴリズムに基づき、AST の tree edit-distance を計算する
- 専用パーサーにより Python、Java、Verilog、Fortran、C/C++ をサポートする
- example AST differences gallery がよく整理されている
- 情報を tuple 形式で出力し、datalog に活用できる
- autochrome は Clojure 専用の diff ツールで、dynamic programming を使用する
- 視覚的説明と例の walkthrough が非常に良い
- Tristan Hume の Designing a Tree Diff Algorithm Using Dynamic Programming and A* は tree diff アルゴリズム設計の記事として参照価値が高い
望むワークフローと最小スコープの計画
- 主なユースケースは LLM 出力のターンごとのレビュー であり、agent に一度に 1 万行を超えるコードを無秩序に生成させるつもりはない
- agent には範囲の定まった作業を任せ、数分後に戻って全体の変更概要を見たうえで、Emacs で自分で修正したり、全部捨ててやり直したり、最初から自分で書き直したりしたい
- 望むワークフローは、どの型・関数・メソッドが追加・削除・変更されたかという 高レベルな概要 をまず見ることである
- その上で各エンティティごとのテキスト diff を素早く展開して見られ、要約から詳細 diff へ自然に掘り下げられる必要がある
- また変更箇所を別の場所へ移動せず、その場で修正できること、つまり diff 画面から file 画面に切り替えない インライン編集 を望んでいる
- 目指すのは、Magit の変更レビュー・staging ワークフロー をファイル・行単位ではなくエンティティ単位へ移したものだ
- 今回あらためて思い出した最小スコープの教訓に従い、まずは Rust だけを対象に treesitter ベースのエンティティ抽出フレームワーク を自分で急いで作る予定である
- マッチングはまず単純な greedy 方式から始め、diff はコマンドラインにレンダリングするつもりだ
- これで特定のコミットにおいて difftastic より良い結果が出るなら、その後は Magit のようなより対話的な Emacs ワークフローにつなげたい
- 可能なら Magit 自体を再利用する可能性も残している
- 新しい言語サポートは必要になった時点で追加するつもりである
- その後は単純な greedy ではなく、スコアベースの グローバルマッチング も探れるかもしれない
- 十分に満足できれば公開するかもしれないが、GitHub star や HN karma を集めることは目的ではなく、ひとりで静かに使うツールのままにする可能性もある
- ときには、ただ棚ひとつが欲しいだけなのだ、という一文で、過剰な拡張ではなく必要なものだけを作る姿勢 をもう一度まとめている
1件のコメント
Hacker Newsのコメント
これは PhD研究 の最大の難しさをよく表していると思う
面白いテーマを見つけて、関連する先行研究をできるだけ全部読んでいくと、自分がやろうとしていることがすでにどれだけやられているかに気づいてしまい、scope creep がひどくなりやすい
最初の勢いと高揚感を使い果たした後は、残りの20〜30%を無理やり押し切って、出版可能な状態まで持っていかなければならない
400日目には万物の理論をほぼ説明し終え、既知の宇宙のあらゆる力を媒介する普遍粒子を検出しようとして ラグランジュ点軌道実験装置 を作ろうとしている
これをどうやって和らげればいいのか気になる
実際にはシステムの観測可能性を1%から1.001%に引き上げるような作業で、学術キャリアに入るための関門に近い
だから本当に面白い内容や、非常に新規性の高い内容や、科学に直接応用可能な内容を含む学位論文はほとんど見たことがない
実際にそんなやり方で研究している人はほとんど見たことがなく、普通は論文を2〜3本ほど読んで、そこから積み上げていくのが自然だ
研究文献を深く洗うのは、ある程度結果が出てから、文章としてまとめ始める段階でやるほうがよいと思う
より良いもの であれば十分だ、という言葉がずっと頭に残る
小さな改善でも時間とともに積み重なるし、最初から完全に新しいものなどないのだから、座って完璧な設計をひねり出そうとする態度はむしろ逆効果が大きい
障害こそが道になる、という言葉もここによく当てはまる
一緒に働いていた同僚は、コード変更を批判するときでも、自分が些細なことをつついていると感じたら「前よりは良い」と言っていた
そのおかげで改善点は指摘しつつも、細かい欠点が残っていてもひとまず先に進んでよいという許可も同時に与えてくれていて、こういう姿勢を強く支持したい
以前は完璧主義を、過度に高い達成を無理に追い求めることだとばかり思っていたが、完璧でなければ受け入れられず、進展もないまま諦めてしまうのも完璧主義なのかもしれない
大きな仕事を先延ばしにする procrastination も、同じ根っこを持っていることが多い
Rec RoomのCEOの言葉が良かった
チームはいつも、プロジェクトをもっと短くできればよかったとは言うが、リリースをもっと遅らせて、もっと複雑にして、もっと磨き込めばよかったと言うことはほとんどないらしい
どんな状況にも100%当てはまるわけではないが、失敗するとしても、あまりに大きく広げて時間を無駄にするよりは、小さく作って早く出す ほうがよいと思う
人間は本性として似たようなアイデアを思いつきやすいので、何も知らずにプロジェクトを完成させると、結局ある程度は 再発明 になりやすいと思う
逆に先に調べると、部分的には既存のものの繰り返しだと気づいて気が抜けることもある
それでも、自分の学習のために最後まで作り切ること自体がいちばん大事なのかもしれない
もちろん、新しい学術的成果を出さなければならない場合や、独自のプロジェクトで収益を上げなければならない場合はもっと難しいが、そうした領域でも既存のものを少しひねるだけで、意外とかなり寛容だったりする
まさに今、サイドプロジェクト でこういう状況を経験している
分野が Information Retrieval なので経験が浅く、学べる prior art や統合できる prior art が当然たくさんある
だからこの記事を読んで、まずは自分のものを作りながら、行き詰まったりアイデアが必要になったときだけ先行事例を見るほうに気持ちが傾いた
一方で最近出たClojureのドキュメンタリーを見ると、Rich Hickey はむしろ長い時間をかけて先行研究、論文、ほかの言語を深く掘り下げてから取り組んだように見えた
でも彼もその前にすでにほかの言語を作っていたわけで、より大きな流れとしては、結局自分で作って学ぶところから始まっていたとも言える
長く考えすぎるのではなく、まず作ってみて、実戦の中で教訓を積み、壁にぶつかったあとで、もっと深い調査が必要になるのではないかと思う
続けて "Easy made Simple", "Hammock Driven Development" まで見たら、Clojure を学びたくなった
Clojure documentary on CultRepo channel: https://www.youtube.com/watch?v=Y24vK_QDLFg
Simple Made Easy: https://www.youtube.com/watch?v=SxdOUGdseq4
Hammock Driven Development: https://www.youtube.com/watch?v=f84n5oFoZBc
締め切り を設けると、scope creep の問題の大半は解決した
体感では、game jam やプログラミングコンテストのようにハードな締め切りがあるプロジェクトは終わらせやすいが、終わりが開かれているプロジェクトは完走がずっと難しい
C++標準が、欲しい機能がすべてそろうまで待たず、なぜ3年ごとに出しているのかという話にも近い文脈に見える
https://news.ycombinator.com/item?id=20428703
文章自体は興味深かったが、著者の考えが少し 散漫に広がっている ようにも見えた
scope creep に圧倒されていると言う人にしては、記事の最後にありとあらゆるテーマのリンクが大量についているくらい、ものすごく多くのことをやれている ように見える
結局、学んだりいろいろ試したりすることが本当に好きなタイプで、rabbit hole に落ちていく過程そのものが頭を楽しく刺激しているのだと思う
一人で作っている立場で大きく助けになった気づきがひとつある
必須の抽象化 に見えるものの大半は、名前を変えた scope creep にすぎなかった
新機能ごとに flag を付けていたら、自分のコードの中にパターンが見えてきたので、ひとつルールを作った
機能は flag-off の動作に対するテスト がなければデプロイしない、というものだ
すると flag を逃げ道ではなく製品の一部として見るようになり、バックログにあった機能3つは、そう考え始めた途端に自然と消えていった
過度な計画と scope creep が問題なのはその通りだが、逆にあまりに 即興開発 側へ振れすぎるのも警戒すべきだ
いちばん成功したプロジェクトのいくつかは、実際に動くソフトウェアを作る前にデータをモデリングしながら機能の大半を先に計画・検討していたケースだった
その段階では何がやりすぎなのかよく分からないことが多く、自分やユーザーが欲しがりそうな機能を外すと、後でコードの中核を大きく再設計するのに時間をかなり使う
逆に読み違えると、プロジェクトが大きくなりすぎて scope creep と呼ぶことになる
結局この判断は、そのドメインをどれだけよく理解しているかにかかっている
ドメインを思ったより理解していなければ手戻りが増えるし、思ったより理解していれば、本当は大きく進められたのに baby step で時間を無駄にしてしまう
どちらに転んでも後悔は残るので、結局は大きな 判断の問題 だと感じる
サンクコストの誤謬 に陥ってはいけないし、博士課程レベルのテーマを何時間か調べたからといって、それをプロジェクトに必ず使わなければならないわけではない
今の問題にぴったり合わないなら、思い切って捨てるのが正しい