メモ: どこに何を置くか #

このリポジトリで、コードやテストをどこに置くと後で分かりやすいか、なぜそうするかのメモ。言語には依らないので、新しい言語の実装 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 がある。