端末に住む
二つ目のツールは小さな roguelike —— マップを歩き、gold を拾い、徘徊する ghost から逃げるキャラクタ。runtime に欠けた一つの能力を強制するために選ばれた:行を待たず、キーが押された瞬間に 1 キーを読むこと。それを成立させた scoping の決定は、ゲームを turn-based に保つことで、対話的なプログラムをキー入力を流し込んでテストできるものに変えた —— CI で走るゲーム。
二つ目のツールは意図して別の方向に行った:外の他プログラムへでなく、内へ、プログラムが既に走って
いる端末へ。小さな roguelike —— mrog —— 単一の @ が壁のマップを歩き、gold を拾い、徘徊する
ghost から逃げる。ゲームの何もが重要でなかった;重要だったのは、対話的な端末プログラムがキーが
押された瞬間に 1 キーを読む力なしに存在できず、言語は一度に行全体しか、ユーザが Enter を押した後に
しか読めなかったこと。それは名前や数を読むには結構な仕方で、キャラクタを操るには無用な仕方だ。gap
は raw な端末入力で、そしてゲームはそこへ歩み込む最も正直な仕方だった。
gap を小さく保つようツールを scoping する
一行書く前に、その後の全てを形づくった設計の決定があった:ゲームを厳格に turn-based に保つ。キーを 押すまで何も動かない;ghost はあなたが一歩進むときだけ一歩進む。これはゲームへの制限に聞こえ、実際 そうだが、その真の目的は言語作業を制限することだった。リアルタイムのゲームは非ブロッキング read、 タイマー、決して眠らないイベントループを要しただろう —— 能力のまるごと一群、各々が自身の調査だ。 turn-based のゲームはちょうど一つの新しいものを要る:1 キーのブロッキング read。二つ目の gap を引き 込む全ての機能を拒むことで、ツールは狙いを単一の的に保った。これはそれ自体名づけるに値する習慣だ: dogfood の価値は一つの欠けた能力を清潔に強制するときに最も高く、おもちゃを scoping することが教訓を ぼやけさせない道だ。
raw mode と、端末を元に戻すこと
能力自体は三つの小さな builtin だった:端末を raw mode にするもの、それを復元するもの、1 キーを読む もの。raw mode は、キーが行にバッファされるでなく即座に届く端末設定で、押されたキーが自らを印字 しないよう echo が切られる。それがどう建てられたかの二つの細部が、機能そのものより重要だと判明した。 一つ目は raw mode で割り込みシグナルを生かしたまま残すこと、そうすれば Control-C がただのもう一つの キー押下として飲み込まれるでなく、依然としてはまったゲームを止める —— 端末を捕らえるプログラムは ユーザに逃げ道を負っている。二つ目は、本物の端末が繋がっていないとき —— 入力が人でなくパイプから 来るとき —— raw mode が単に何もしないこと。その二つ目の選択は些細な堅牢性の気配りに見える。それが ツール全体をテスト可能にしたもので、自身のセクションに値する。
CI で走るゲーム
raw mode は入力がパイプされるとき no-op に退化するので、完成したゲームは丸ごとスクリプトから駆動 できた:「右、右、右、終了」というキー入力を与えれば、それらの手を打ち最終状態を印字する。つまり 対話的なプログラム —— 存在する理由の全てがリアルタイムのキーボード入力だったもの —— を非対話的に 走らせ、その出力を捕捉し、同じ入力を走らせる interpreter に対しバイト単位で照合できた。ゲームが回帰 テストになった。三つの缶詰シーケンス —— gold を集めるもの、壁に歩み入り止まるもの、手を混ぜるもの —— が interpreter と native バイナリに跨って振る舞いを固定した。味わうに値する小さな皮肉だ:言語が持つ 最も頑固に対話的な能力を行使するために建てられたツールが、コレクション全体で最も機械的にテスト 可能なプログラムの一つでもある、まさに raw な入力が人が見ていないとき消えるよう設計されたがゆえに。
interpreter だけの一家、再び
native build は task runner が見せたのと同じ種の縫い目を surface した。末尾改行なしの印字は interpreter で動いたが C backend には実装されたことがなかった;ghost の徘徊のための乱数生成も同じ だった。共に型検査を通り、共に interpreted で走り、共にコンパイルされて初めて欠けた —— 前のツールの 欠けたエラーチャネルやファイル検査と同じ失敗の形だ。今やこれは見覚えのある一家だった:interpreter にだけ静かに住み、それを要る最初の native プログラムを各々待っていた一握りの小さな runtime 機能。 roguelike がそのうち二つを更に見つけた。個々の修正に賢いものは何もない —— 各々が backend を interpreter が既にしたことに繋ぐ数行だ —— が、パターンこそ実の結果だ:本当に違う形の native プログラムを建てることが、さもなくば在ると知りもしなかった parity バグの溜め池を抜く道だ。
終わりまでにゲームは native バイナリとして遊べた —— 移動、衝突、収集、そして捕まえたら run を終わら せる ghost —— そして三つのリリースを駆動し、その一つ一つが今や全てのプログラムに属する能力だった。 だがより深い収穫は方法についての二つの教訓だった:ツールをきつく scoping することが単一の gap に狙いを 保ち、機能を優雅に退化するよう設計することが対話的なプログラムさえテスト可能にする、という。次の ツールは端末も他プログラムも要らない。それは raw なバイトを読む —— そしてそれが読むと選んだバイトは、 コンパイラ自身が emit するものだった。