メモ: どこに何を置くか #
このリポジトリで、コードやテストをどこに置くと後で分かりやすいか、なぜそうするかのメモ。言語には依らないので、新しい言語の実装 foo/ を足すときの目安にもなる。例には moonbit/ を使う(go/ も pkg/、examples/、Makefile は同じ形で、experiments/ はまだない)。
守るべき規則ではない。数か月後の自分が見て、どこに何があるか分かれば十分。作業の途中は散らかっていてかまわない(moonbit/ も、ブログを移植している間はモジュールの直下にパッケージが並んでいた)。落ち着いたところで、ここに書いた形に寄せる。
新しい言語を足すときの目安 #
foo/
README.md 何がどこにあるか、どう動かすか
Makefile 言語の道具の外にあるものをつなぐ糊(build、test、check)
pkg/ ライブラリ。保守対象
examples/ アプリ(ブログなど)。保守対象
experiments/ 実験。保守対象のモジュールの外に置き、そちらから参照する- 言語の道具がモジュールやパッケージを持つなら、
foo/を 1 つのモジュールにし、pkg/とexamples/をその中のパッケージにすると分かりやすい。実験は別のモジュールにしておく。 - テストのための部品(スタブのアプリ、フィクスチャ)は、テストされる側の隣にあると探しやすい(後述)。
- Makefile のターゲットの意味が言語ごとにそろっていると、どの実装でも同じつもりで
make checkを実行できる(後述の表)。 - その言語の道具に合わないところは、合わせなくてよい。気づいたことがあれば、この文書に書き足す。
置き場所は「誰が、どれくらい長く使うか」で決める #
コードを 3 種類に分けて考えると、置き場所が決めやすい。
| 種類 | 置き場所(moonbit/) | 保守 | テスト | 公開 API |
|---|---|---|---|---|
| ライブラリ | pkg/(el、picoel、tinymarkdown、tinyfrontmatter、http、http/wasm) | する | 常に走らせる | ほかから使われるので、必要なものだけに絞る |
| アプリ | examples/blog/(site、worker、compare など) | する | 常に走らせる | ほかから使われないので、ほとんど要らない |
| 実験 | experiments/(adapter-abi、wasm-fetch、http-bench) | しない | 走らせない。ビルドできることだけを確かめる | 要らない |
- ライブラリとアプリは保守対象で、変更のたびにテストを走らせる。
- 実験は突発的な探索で、計測の数字を再現できるように残しているだけ。古い形のままであることに意味があるので、整えたり書き直したりしない。ただし、ビルドできなくなったら直す。
- 迷ったら「これが壊れたとき、誰が困るか」を考える。誰も困らないなら実験、ほかのコードが困るならライブラリ、使う人が困るならアプリ。
依存は一方向にする #
依存は、実験 → アプリ → ライブラリの向きにだけ張っておくと、どこを直すと何が壊れうるかが追いやすい。
pkg/の下はexamples/を参照しない。- 実験は、保守対象のモジュールの外に別のモジュールとして置き、実験の側から参照する。
moonbit/では、実験ごとにmoon.modを置き、moon.workでブログのモジュール(../..)をメンバーにしている。保守対象のモジュールは実験の存在を知らない。 - このため、保守対象のモジュールで
moon checkを実行しても、実験は見えない。公開 API を変えると、気づかないうちに実験が壊れることがある。実際、Router::newをRouter::Routerにしたときにwasm-fetchが壊れ、全体の検証の後まで気づかなかった。そこでmake checkが、実験の各モジュールでもmoon checkを実行する。 - 実験を保守対象と同じワークスペースに入れる(ルートに
moon.workを置く)と、moon checkだけで実験も見える。ただし、ふだんのmoon testやリリースのビルドにも実験が混ざる。依存の向きを優先して、そうしていない。
テストのための部品は、テスト対象の隣に置く #
名前は形ではなく役割で付け、置き場所も役割で決めておくと、後から見たときに何のためのものか分かる。
pkg/http/wasm/integration/は、http/wasmの integration test。echo.mbtは受け取った要求をそのまま返すスタブのアプリで、test.mjsがそれを Node でadapter.mjs越しに動かす。JS と Wasm の線形メモリの間の受け渡しを確かめているのは、このテストだけ。ブログの Worker も同じ経路を通る。- 最初は
examples/echoという「使用例」として置いていた。ただ、日常的に使うツールでも、一度きりの例でもなく、実態はテストの部品だった。そこで、テスト対象のpkg/http/wasmの下に移した。 - スタブは本番のビルドに入れない。最適化(wasm-opt)も要らない。
make testは moon の出力をそのまま読む。
公開するものは、呼ばれているものだけにする #
- パッケージの外からテストしか呼ばない関数は、API ではない。非公開にして、テストは white-box テスト(
_wbtest.mbt)にする。http/wasmのdecode_requestとencode_headがこれに当たった。アプリが使うのはhandleだけ。 - 汎用の部品として外から使われうるものは、今は呼ばれていなくても残す。
httpのmedia_typeとparse_valuesは、Go のmime.ParseMediaTypeとurl.ParseQueryに当たる。 - 一覧は
pkg.generated.mbtiで見る。make checkはmoon infoの差分で止まるので、公開 API の変更は diff に出る。
言語の道具と、それを補う糊 #
- パッケージとモジュールの扱いは、言語の道具(moon、go)に任せる。一部だけ動かすときは、
moon test -p podhmo/html-sandbox/pkg/httpのように言語の道具を直接使う。 - make は、言語の道具の外にあるものをつなぐ一時的な糊。
moonbit/では、Static Assets に置く記事と文書のコピーと目録(content)と、テスト用の埋め込みの生成(embed)、wasm-opt、Node でのテスト、wrangler が当たる。 - make のターゲットは保守対象の全体を扱う。
| ターゲット | 内容 |
|---|---|
make build | 配備するものだけ(ブログの Worker)をビルドし、wasm-opt をかけ、Static Assets に置く記事と文書をコピーする |
make test | 保守対象のテストをすべて走らせる(moon test と、Node での integration test) |
make check | コミットの前の簡易チェック。make test に加えて、fmt、check(wasm-gc と native)、実験のビルド、.mbti の差分 |
- make で何かを補う必要が出たら、まず構成を疑う。たとえば一時期、
make testが実験の各モジュールでもmoon testを実行していたので、ブログのテストが 3 回走った。実験はmoon.workでブログを含むので、実験の中のmoon testはブログのテストも走らせる。実験はテストせず、ビルドだけを確かめる、という区別がないまま糊で補おうとした結果だった。
名前は、持っているものに合わせる #
- モジュールがブログ以外のライブラリも持つようになったので、モジュール名を
podhmo/blogからpodhmo/html-sandboxに変えた。パッケージのパスもpodhmo/blog/examples/blog/siteのような重なりがなくなり、podhmo/html-sandbox/examples/blog/siteになった。
見直すきっかけ #
次のことが起きたら、置き場所か依存の向きを見直す頃合い。
- make が同じ処理を何度も実行している、あるいは言語の道具でできることを make が代わりにしている。
- 検証を通っているのに、何かが壊れている。
- 「例」や「ツール」と呼んでいるものが、実際にはテストからしか使われていない。
- パッケージの外からテストしか呼ばない
pubがある。