考えすぎ、スコープ拡大、構造化diffでプロジェクトを台無しにする方法
(kevinlynagh.com)- プロジェクトは すぐ作って終わらせる流れ と、調査や設計が膨らんで元の問題を見失う流れに分かれやすく、実際の前進では とにかくやってみる側 のほうが先に進むことが多い
- 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 がアンカー機能にも対応していたことだった
- ファイルパスだけのコーパスでは行頭アンカーは役に立たないように見えたため、これを path segment 基準のアンカー として解釈しようとした
- 例えば
^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 アルゴリズム を使っている
- データモデルはファイル内移動を検出できず、そうした移動は重要な意味を持ちうる
- 信頼するにはより強い言語統合が必要そうな、heuristic ベースの 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 画面に切り替えない inline 編集 を望んでいる
- 目指すのは 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 で時間を無駄にしてしまう
どちらに転んでも後悔は残るので、結局は大きな 判断の問題 だと感じる
サンクコストの誤謬 に陥ってはいけないし、博士課程レベルのテーマを何時間か調べたからといって、それをプロジェクトに必ず使わなければならないわけではない
今の問題にぴったり合わないなら、思い切って捨てるのが正しい