2024年、私がプログラミングする方法
(akkartik.name)- 2年間自分で使いながら直してきたプログラムの中核を、この1か月で書き直した経験により、テストとバージョン管理についての従来の信念が揺らいだ
- 2015年には悪い抽象化よりも テストとバージョン が長く使われるソフトウェアの核心だと考えていたが、Mu と Freewheeling Apps を経る中で、実際の作業方法は次第に変わっていった
- 長く続くプログラムは、多くの人を対象にするよりも、よく知っている人・文脈・機能の中で作り、Dunbar's number のような現実的な限界を受け入れるべきだと考える
- 型、抽象化、テスト、バージョン、状態機械、不変性、形式的分析は未知の領域で有用だが、行き過ぎると不要な複雑さを隠す 技術的負債 になる
- 文脈理解が安定したら、大きな部分を捨てて作り直す価値があり、必要なシナリオを一度に頭の中に載せたうえで、全体を一度に構成 しなければならない
テストとバージョン管理に対する考えの変化
- 長く頼れるプログラムを選び、自分で作るという問題にずっと取り組んできたが、自分でもそれが得意だとは感じていない
- 最近の1か月間で、2年間使いながら段階的に修正してきたプログラムの 中核を書き直した
- その後の数日間は、何を学び、次にどこへ向かうのかを整理する時間が続いた
- 今回の作業をきっかけに、より広い人生上の変化が見え始めた
- 2015年には抽象化を疑い、テストとバージョン管理を重視していた
- 2017年には Mu1 を現在の Mu として再び作り始めた
- 初期 には、テストとレイヤーに関する新しいアイデアをすべて使っていた
- 時間がたつにつれて、それらのアイデアはあまり使わなくなった
- 現在の Mu にはテストが多いが、その大半は 一般的なテスト であり、レイヤーのインフラは移植できていない
- 2022年には Freewheeling Apps を作り始めた
- 当初はテストがなく、後になって中核部分であるテキストエディタについて徹底したテストを書いた
- それ以外をどうテストするか見つけるのは難しかったが、テストなしでも十分に進められた
- 2024年にはすべてのテストを削除した
- テキストエディタを大きく作り直し始め、このやり方だと他の Freewheeling Apps とのマージ衝突が気になるはずだった
- 結果として、バージョン管理についての考えも止まった
- テストとバージョンを手放したあと、より良いプログラムが得られたことで、従来の信念との 認知的不協和 をこれ以上無視しづらくなった
長く続くプログラムのための現在の整理
- 多くの人のために長く使われるものを作るのは難しすぎるので、最初からそれを目指さないほうがよいと考える
- 自分がよく知っていること、よく知っている人、Dunbar's number に支配されるべきだ
- 世の中の大半のソフトウェアは、短期的に多くの人に奉仕しようとするインセンティブに汚染されていると考える
- できるだけウェブサイトにロゴが多くないソフトウェアに集中する
- 作りやすく、依存関係が少なく、自動更新をしないソフトウェアを好む
- こうした制約でふるいにかけると、人類がこれまで作ってきた 長く続くソフトウェア の量はごくわずかになる
- 人、場所、支援したい機能といった文脈の小さな変化でも、プログラムがその文脈にどれだけうまく適合するかを大きく変えうる
- 短期主義が支配的な環境では、この事実に備えるのは難しい
- 過去の作業量は小さく、プログラムごとの適用範囲も低いため、新しく作ることにしたプログラムは、何らかの形で未知の領域に入る可能性が高い
- テキストエディタに特殊な「drawing lines」を入れようとするときでさえ、さまざまな疑問が生じる
- カーソルは図の上に置けるのか
- カーソルが別の行にあるときに、ある行へ図を描けるのか
- 図はテキスト行より高いが、画面上部で部分的にしか見えないことはありうるのか
- 部分的に見えている図の上に描けるのか
- これらの問いへの答えは長いあいだ最適ではなく、その場しのぎの上にまたその場しのぎが積み重なった
- テキストエディタに特殊な「drawing lines」を入れようとするときでさえ、さまざまな疑問が生じる
道具は必要だが、行き過ぎると技術的負債になる
- 型、抽象化、テスト、バージョン、状態機械、不変性、形式的分析は、未知の地形で使える 道具 である
- 必要なだけ、自分の好みに合わせて使えばよい
- 人は自分が惹かれる道具を過剰に使いやすい
- これらの道具の理想的な使用量は非常に少ないと考える
- 短期主義が支配する環境で身につけた感覚より、はるかに少なくあるべきだ
- 道具を使いすぎると 技術的負債 になる
- プログラムが不必要に複雑だという事実に気づきにくくなる
- プログラムは、もっと長持ちできる状態よりも長持ちしなくなる
- 文脈が変わったとき、プログラムを変更するのがさらに難しくなる
書き直しと「一度に全体を作る」こと
- 文脈についての理解が安定したら、プログラムの大きな部分を捨てて最初から作り直すことには価値がある
- 書き直す前には、プログラムに求めるすべてのことと、対応しなければならないすべてのシナリオを一度に頭の中へ持ってこなければならない
- この過程は難しいが、目標はすべてを一度に作れる状態に到達することだ
- 最終的なやり方は、すべてを一度に作ること である
- 今回の経験では、テストとバージョンがこの進化の終点に到達するうえで、むしろ妨げになった
- テストは、心配すべき問題を忘れさせる
- バージョン管理は、過去に引き留め続ける
- どちらも逆効果で、それらを手放すには大きな方向転換が必要だった
- これまで作ってきたすべてのソフトウェアと Freewheeling Apps は、この軌跡の第6段階にあると考えている
- この1か月の成果物 だけは第9段階に到達したかもしれない
- ただし、その判断はまだ見守る必要がある
複雑性の限界とデータ指向設計
- プログラムが複雑になりすぎると、第8段階でそれを丸ごと頭に載せることが不可能になるかもしれない
- これまでのソフトウェアの大半、特に2〜3人より多い人数で書かれたソフトウェアがこれに当たると考える
- 小さなテキストエディタでさえ負担が大きく、この1か月の多くの時間をその恐れに立ち向かう準備に費やした
- すべてのソフトウェアが必ず第9段階まで行く必要はない
- 多くの Freewheeling Apps は十分に単純で、ゆっくり進化する
- 少数の人に使われるだけでも、初期の設計選択とは無関係に、バグのない状態へ安定化できると考える
- 特に今では、中核にある複雑な断片の1つを単純化する方法を知っている
- それでも、価値が生まれたときにどう改善できるかを知っておくのはよいことだ
- 第9段階に到達するのに確実に有用そうな方法として、data-oriented design を挙げる
- これは盲目的に適用できる道具ではなく、プログラムがデータにどうアクセスするかを大局的に見るための考え方である
- ECS のような道具が、本質的な知的活動を覆い隠さないようにしなければならない
- この段階区分は完全には正しくないかもしれない
- 経験の少ない道具を過小評価している可能性がある
- この段階の先に何があるのかも、まだ開かれた問いとして残っている
- 2019年に書いた プログラミングのやり方に関する文章 以後、考えが変わってきた痕跡が見て取れる
1件のコメント
Hacker Newsの意見
テストがなければテスト失敗が見えないので、問題が消えたように見えるだけです
何かをテストしてバグを見つけられなかったことはなく、テストしたものの大半は、すでにリリースしてよいと思っていたものでした
テストを消せば、結局だます相手は自分だけである可能性が高いです。記事を読むと、テストそのものというよりバリエーション/設定管理に疲れているように見え、その点は十分共感できます。ただ、ユーザー数があってこそお金になり、簡単な問題だったなら、すでに万能ソリューションで市場は飽和していたはずです
UIやワークフローがあまりに速く変わると、次の反復では役に立たなくなると分かっているのでテストを書かなくなりますし、逆にあまりにゆっくり変わるなら、その部分を再び触ることがなく、リファクタリングで新しいバグが入ることも少なくなります。テストや型は万能の聖杯ではなく、作業に合った道具です。良いテストカバレッジがあっても、手動テストや実際の利用でバグが見つからないコードベースを見たことはありません。少し大げさに言えば、完璧なテストを書けるほど優秀なら、そのまま完璧なコードを書けばよいのです。テストを完璧に書けないなら、そのテストが完全で、バグがなく、本当に有用だとどうやって分かるのでしょうか
数か月ごとに新しいバグを見つけて律義にテストを追加してきましたが、数か月後に初めて10分使った人がまた新しいバグを見つける、ということがよくありました。新バージョンにも見つかるバグはあるでしょうが、選んだデータ構造のおかげで、以前のテストのかなりの部分は構造的に不要になったと考えています。少なくとも軽い利用なら、あといくつかバグを潰せばかなり安定することを期待しています。テストは大きなチームがコードベースを継続的に変更するときには非常に貴重ですが、ここでは固定された機能セットを持つものを作ろうとしているところです
有用な助言が非常に多いですが、特に触れたいのは、より単純な実装、たとえば総当たり実装を基準にテストできるという点です。ここにはより深い知恵があります。テストの有用性は、テスト実装がテスト対象の実装よりどれだけ単純かに依存します。さらに強く言えば、テストはテスト対象より単純なときにだけ有用です。どれほど多くのテストを書いても、結局はコードを推論しなければならず、何かが「テスト」であるという事実だけで有用になるわけではありません。だからこそ多くのプログラマーは、テストを書きやすくするために有用なインターフェースではない断片へ関数を分割したり、カバレッジのためだけに単純なヘルパーや小さなクエリをテストしたり、テストのためだけに抽象化を導入する依存性逆転とモッキングを警戒しているのだと思います。もちろんそれぞれに理由はあり得ますが、核心を見失わないことが重要です
普段は失敗するテストを先に書くテスト駆動開発方式はあまりやりませんが、たまにはやります。なので、こうしたテストはたいてい、すでに動くと思っていたコードを対象にします。ただ、通常は単体テストよりテストハーネスを好みます[0]。それでもバグは見つかりますが、流れはそれほど一直線ではありません。開発中により多くテストするよう促し、その場でバグを直せるようにしてくれます
[0] https://littlegreenviper.com/testing-harness-vs-unit/
たとえばLinuxカーネルは昔はテストが多くなく、最近は増えているようです。Unixにも「テスト」は多くなかったでしょう。コンパイラにはテストがあるほうでしたが、オペレーティングシステムは少なめで、Doomのようなゲームもテストは多くなかった可能性が高いです。結局はバランス点を見つける必要があります。自動化テスト、つまり単体・統合・エンドツーエンドテストが品質の高いソフトウェア作りに役立つことは分かっています。同時に、良いテストは常に書きやすいわけではなく、悪いテストはリファクタリングを難しくし、不安定なテストは大規模プロジェクトで多くの時間を奪います。それでも、特に一人で開発しているなら、いろいろな方法を試して自分に合うものを見つけるのは興味深いことです
「テストとバージョンを諦めたら、ずっと良いプログラムになった」という部分は理解しにくい。2024年に、誰が自発的にソースコード管理なしでプログラミングしようとするのか分からない。
1人プロジェクトでも複数の端末で作業し、履歴を見て、戻して、ブランチを使える能力は、ほとんどコストなしに非常に大きな価値をもたらす。おそらく著者が「バージョン」で意味したことを私が誤解しているのかもしれない
このアプローチが、今日人々が作る大半のプログラム、つまり大規模チームと変わり続ける要件には合わない、というのはその通り。それでもソース管理は今も使っている。原文で述べたように、他のフォークとマージコンフリクトを起こす心配をやめただけだ。今ではフォークが24個を超えており、詳細は上のリンクにある。バックアップ、「今何を変えたんだっけ?」、新しい端末にソフトウェアを載せる、といった基本用途にはバージョン管理を使っている。ただしこのプログラムに限っては、バージョン管理を、何が変わったのかを理解し追跡する手段とは考えなくなった。詳細は https://akkartik.name/post/wart-layers にある。例えば、コミットメッセージのきれいさをあまり気にしなくなった。バージョン管理は存在するが、機能セットが固定され、何十年も続く耐久性のある成果物にしようとするこの狭い文脈では、「良いプログラミング慣行」としての優先順位が下がった
大規模システムを扱っている、あるいは重要なチーム作業をしている状況でもなさそうだ。こうした条件では、ツールが大きな価値をもたらさないこともある。複雑な交響曲を演奏する大きなオーケストラのフルート奏者には楽譜と指揮者が必要だが、ドラムマシンに合わせて1人で演奏したり、フリージャズをやったりするなら、楽譜はあまり必要なく、むしろ邪魔になるかもしれない
しかし簡単な選択肢が常に1000個あると、正しい選択を選ぶために大きな認知負荷が生じる。業界があらゆるベストプラクティスを神聖視し、それに従わない人に社会的圧力をかける理由の一つもそこにある。悪いアーキテクチャやひどいスパゲッティコードは非常に仕事をしづらくするが、当然正しそうに見えるものを疑い、選択肢とツールを減らす厳格な開発環境を探ると、最終的な問題により集中できる。バージョン管理も、ブランチがプログラムを「独立した機能」に分割するよう促し、履歴は古くなっているかもしれない機能単位を盲目的に使わせ、協業はたいてい無関係な組織の境界をコードアーキテクチャに固定化する。Mel Conway が述べたことにも通じる。バージョン管理の利点は常識だが、「ビジネス課題 X を解決する」というレベルでは実際のトレードオフが存在する。業界全体でこうしたトレードオフがほとんど見えないのは示唆的だ
最初は著者が完全に間違っていると思ったが、それでも良い洞察はいくつかある。
このワークフローは著者にはとてもよく合っている。私たちの多くも、Git や自動テストのせいで挫折したり生産性が落ちたりした時を思い出せる。Dropbox、FTP などでコードをバックアップするような、より単純で邪魔にならない解決策もある。上のやり方がうまく合っている理由は、著者が少人数と協業する個人的な愛着のあるプロジェクトで自分の生産性を最適化しているからだ。自動テストは有用だが、著者はその価値が見えにくいほど小さなプログラムを作るのが好きなようだ。この文脈でも自動テストの価値はあると思うが、自動テストが速度を落とすことには誰もが同意できる。もちろん多くの人は、後で見返りがあると主張するだろう。バージョン管理と自動テストは実際の問題を解決する。今日、バージョン管理なしでプロジェクトを始めるのはあり得ないし、自動テストがベストプラクティスであるのには理由がある。ただ、著者の特定のユースケースでは合理的に聞こえる。議論の多いバージョン管理/テストの部分を除けば、項目 7/8/9 は、大きなプログラムを書いてリファクタリングするときの私の思考様式を完璧に捉えている。書き、捨て、また書く
人はミスをするし、10万行を超えるプロジェクトで、この3週間に何を変えたかを知るのは大いに役立つ。問題を見つけて直すのに有用だ。さらに良い機能としてブランチがあり、以前の安定状態に戻る方法を維持したまま、やりたいことを試せるようにしてくれる。自動テストはなくても構わないと思う
.gitignoreを設定し、git init、git add -A、git commit -a -m "before I changed the foo function to use bar"くらいを実行して以前のリビジョンに戻れるだけのGitを学ぶ時間には十分価値がある。Git をマスターする必要はないが、コミットメッセージと戻れるバージョンがあるだけで、数えきれないほど救われてきた。より高度な機能については言うまでもない
かなり混乱した文章だ。何が理由で1位まで上がったのか本当に気になる
妥当なテスト群をそろえる主な動機は、フラストレーションを減らすことです。テスト群は、開発者にシステムを進化させる自信を与えます。
うまく作れば、「残りをテストする方法を見つけるのは難しかったが、とにかく何とかうまくやれた」といった考えもしばしば生まれます。機能の複雑さが増すほど、コンポーネントやシステム全体をテストする難易度は手に負えないものになり得ます。しかし、テストやバージョン管理を捨てたことでより良いプログラムになった、という哲学は、1人を超えてスケールしません。それも、その人がソースコードに含まれる現在と過去のあらゆる判断を、最近の親密な記憶として把握している場合に限られます。さらに実装を深く知っているなら、すべての変更検証は定義上、手動で実行しなければなりません。
1日の終わりにマージ可能な状態になっていなければ、それは1日のうちに表現できるほど問題を十分に理解していなかったという意味なので、翌朝あらためて試した、という内容でした。これを覚えている人がいるのか、それとも別のサイトや逸話と混同しているのかは分かりません。
ドキュメント、テスト、バージョン管理は、コードの文脈について自分が覚えておくべき量を減らしてくれます。目の前のコードの詳細は覚えておく必要がありますが、文書化し、テストし、なぜ/どのように変えたのかを良いコミットメッセージとともにチェックインすれば、そのコードを頭から追い出して次の仕事に進めます。
「支援しようとしている人/場所/機能といった文脈の小さな変化が、そのプログラムがその文脈にどれほどよく合っているかを急激に変える」という3番目の項目の良い例は K9 Mail です。今ではAndroid版Thunderbirdになりつつあります。
K9 Mailは、メールアカウント一覧をホーム画面に表示し、各アカウントの未読メッセージ数と総メッセージ数を表示する、非伝統的なUIから始まりました。統合受信トレイはありましたが、ユーザーに強制はしていませんでした。個人アカウント1つ、仕事用アカウント1つ、顧客から渡された複数の仕事用アカウントを分けておきたくて、このアプリを明示的に選んだ記憶があります。おそらく多くのK9ユーザーも同じ理由でこのアプリを選んだのでしょう。開発者が、左からアカウント一覧がスライドしてきて、アカウント間の移動にもう1タップ必要な伝統的なAndroid UIへ移行したときに不満が多かったのも、そのためです。私たちがそういうUIを好んでいたなら、そもそもK9を選んでいなかった可能性が高いです。つまり、小さな変更1つ、ただし実装量は多かったであろう変更が、ユーザーに合っていたアプリの適合性を損ねました。私は古いUIがある最後のバージョンである 5.600 を使い続けており、新しい端末を買うたびにサイドロードしています。さらに変わっていることに、アカウントアクセスにはPOP3だけを使っています。スマホで事前に確認し、削除するものは削除し、必要なら自分にBCCして返信し、最終的にはノートPCでダウンロードする流れですが、K9はこのワークフローに完璧に合っていました。派手なものは必要なく、90年代のアプリ程度で十分です。
https://news.ycombinator.com/favorites?id=akkartik&comments=t
この道がどこへ続くのか、私もずっと気になっています。1つはっきりしているのは、1人でソフトウェアを作ることは、チームで作ることとはまったく別の活動だということです。
テストについて言えば、テストは目的ではなく手段です。私たちが求めているのは自信だと思います。実装に自信があれば、テストは少なめになります。逆に、必ず動き続けなければならないものがある場合は、リファクタリングの影響を受けにくく、速度もあまり落とさない外側の境界に統合テストをいくつか追加します。内部をテストするというより、Webバックエンドを外側からつつくような形です。単体テストは新しいAPI設計を具体化するのには向いていますが、方向性が分かった後は、それらのテストはほとんど役に立たなくなります。
作っている機能へすぐ行くために
true ||のような形でif文を一時的に固定するのは時間がかかり、後で取り除く必要もあります。単にテストを作って実行すれば、回帰テストとして残せます。大きなアプリや遅いアプリをデプロイするなら、時にはQtを使っているというだけでビルドや実行に時間がかかりますが、そういう場合は単一のテストのほうがより速くロードされ、より速く実行されます。バグの再現に45秒かかるなら、テストを書いたほうがいいです。作業の中で最も退屈な部分を自動化し、流れを保ち、確認する価値があるかを毎回考えなくても望むだけ頻繁にバグの状態を確認でき、やはり回帰テストとして残せます。この著者が本当に好きで、Mu は私のお気に入りプロジェクトの1つです。現代的なLispマシンのようなもの、それもQEMU上で動く面白いプロジェクトです。
「ほとんどのソフトウェアは、短期的に多くの人に仕えようとするインセンティブに、修復不能なほど感染している」という一文が好きです。ソフトウェアを「ビジネス」に置き換えても、そのまま通じます。
私たちは皆、ソフトウェア工学分野の複雑さにある程度圧倒されている。時にはその複雑さが偶発的なものでもある。
しかし、何十年にもわたって生み出してきたあらゆるアイデアを拒否することが解決策だ、という点には同意しない。逆に、すべての解決策を文字どおり受け入れたり、「使いすぎ」たりしてもいけない。圧倒されるというのは、定義上、何かを使いすぎたときに起きるものだ。テストを書き、バージョン管理システムを使い、抽象化を使う。ただし、なぜ使うのかを理解していなければならない。その「なぜ」がもはや成り立たないなら、再評価すべきだ。
これはソフトウェア開発において最悪に近い呪文の一つだ。さらに悪いのは、こうしたものを宗教の域で学んでいる点だ。奇妙なのは、ここ数年で専門家がソフトウェアを書く方法は大きく進化しているということだ。述べたように、抽象化があらゆるものに本質的に悪いわけではない。SQLデータベースに入る典型的なデータに、
updatedやupdated_byのようなフィールドを持たせる基底クラスがない状況は想像しにくくもある。だが一般的には、本当に強制されない限り抽象化はほとんど使わない。それなのに学界では、私が25年前に学んだものとまったく同じカリキュラムを今も教えている。学生たちが立派なUMLで巨大な抽象化を作り、それをコードに実装する能力を採点していると、実に奇妙に感じる。彼らの90%は二度とUML図を一つも見ることはないだろうに。少なくとも、私のいる狭い領域ではそうだろう。それでも現実は現実だ。あらゆるものにコマンドラインレベルの「porcelain」があればいいのにと思う。標準の
--help=ui出力とdialogスタイルのインターフェースがあれば、自動化できそうだ。複雑さに圧倒されているというより、活用できる能動的な筋肉記憶の量には限界があり、どこかで切り捨てなければならない、という問題に近い。