スコープではない寿命

小さなアプリの購読リークが、言語への問いに変わり、答えは「否」だった。出発点にある二つの前提 — 同じ処理なら速い方がよい、GC に依存しない — は買い物であり、Mere にできないことは、その請求書が一枚ずつ届いているだけだ。

merelanguage-designessaymemory-modeltradeoffs

デモアプリにタブバーが生えた。一つのストアの上に二つのビュー — スキーマからコントロールを組み立てる フォームと、読み取り専用のサマリだ。タブを切り替えると古いビューの DOM はまるごと捨てられる。そこは 簡単なところだった。ノードを捨てても watcher は捨てられていなかった。タブを 3 往復すると watcher が 11 個走っていて、そのうち 9 個は、もう document にないノードへ律儀に描き込み続けていた。気づけたのは タブバーにカウンタを置いていたからで、それだけが理由だ。

修正は平凡なものだ。購読の呼び出しがトークンを返すようにして、対応する解放を用意し、ビューは自分の トークンを集めて閉じるときに返す。11 が 2 になり、どれだけ画面を行き来しても 2 のままになった。

面白かったのは、これを言語が防ぐべきだったのか、と問い直したときだ。答えは「否」で、この否は修正 そのものより価値がある。

唯一の機構が、なぜ届かないか

Mere が解放を強制する手段はちょうど一つある。drop 型を with で束縛し、スコープの終端で close が 走る、というものだ。これは良い機構だ。そして購読に対しては形が違う。

ビューはあるイベントの中で組み立てられ、別のイベントの中で壊される。with ブロックがハンドルを閉じる 時点では、そのビューはまだ表示すらされていない。ここで問題になっている寿命は、それを作ったスコープでは ない。イベントループの別々の 2 回にまたがっていて、その間には完全にユーザ次第の、期間の定まらない 空白がある。

もう一つ、静かな障害がある。drop 型は region に置けない。そして watcher のリストは region の上に ある。仮にタイミングが合ったとしても、置き場所が合わない。

だから解放は規約のままにして、形の合わない機構で覆い隠すのではなく、ライブラリの README に書いた。 否の答えを素直に記録しておくことは、言語プロジェクトにできるかなり有用なことの一つで、同時に最も 省略しやすいことの一つだと思うようになった。

二つの前提は、前払いだった

これを「未実装の機能」に分類するのは簡単だが、そうではない。言語の出発点から導かれている。

Mere は小さな前提の集合から始まっていて、そのうち二つがほとんどの仕事をしている。同じ処理を計算する 二つのプログラムなら速い方がよく、コンパイル時に解決できることを実行時に持ち越さない。そしてメモリは GC に依存せずに管理し、所有と寿命は静的に追跡する。

これらは機能ではない。買い物だ。買えたものは本物で、使わなければコストがゼロの抽象、停止時間のない 実行、そして同じ言語がブラウザのタブから、下に OS のないベアメタルの RISC-V コアまで届くだけの小さな 値モデルが手に入った。

支払ったのは動的性で、それを全部、先に払っている。後から現れる制限はすべて、すでに済んだ買い物への 請求書だ。watcher の件は、その請求書が意外に思えるほど具体的な形で届いた一枚だった — どのスコープにも 収まらない寿命が、強制の機構がスコープの形しかない言語に現れた。

より大きな請求書 — すべてがコンパイル時に揃っていなければならない

二つ目の帰結はもっと大きく、日常的には見えにくい。

Mere のジェネリックなディスパッチは型を通して解決される。つまりジェネリックは実体化ごとに具体化され、 具体化には定義本体が必要になる。この一つの事実が、見た目以上に遠くまで波及する。

まず、ジェネリックの境界をまたぐ安定したインタフェースが存在しない。安定したインタフェースがなければ、 コンパイラが見ていないコードを読み込めない。プラグインもなく、後から実装を差し替えることもできず、 誰かが後で書いた拡張を受け入れるホストプログラムも作れない。言語が埋め込みスクリプト層として食べて いく場所はまさにこの枠で、Mere はそこに座れない。まだ、ではなく、構造的に。

そしてサイズとコンパイル時間が、ソースの量ではなく実体化の数に比例する。今年の測定値が二つ、この形を 具体的にしてくれる。セルフホストのコンパイラをフラットな RV32IM 機械語に落とすと約 380KB になる — コンパイラ 1 個ぶんだが、同時に「使っているジェネリックを全部その場で具体化したコンパイラ 1 個ぶん」 でもある。もう一つ、Mere で書かれた Ruby サブセット処理系の mere-ruby は、しばらく 12 個の gem が SIGSEGV していた。メモリのバグに見えて、真因はプログラムサイズだった。128MB のスタックでリンクする まで止まらなかった。同じ請求が、二度、別の通貨で届いたということだ。

循環はキーワードではなく設計を要求する

三つ目の帰結は最も軽い。少なくとも機構が用意されているからだ。

所有権と region の組み合わせは、循環するものや自己参照するものを「ただ書く」のではなく「配置する」 ことを要求する。グラフ。木の親ポインタ。双方向リンクリスト。観測対象への逆参照を持つ observer。 Mere の答えは region の内側に作る束ね型で、immutable かつ移動せず、unsafe に手を伸ばさずに自己参照を 表現できる。これは機能する。そして毎回それを設計する必要がある。GC のある言語なら、素直なものを打って 先へ進める場面で。

これを三つ目に置いたのは、壁ではなく税だからだ。ただし同じ買い物から出た、同じ税である。

5 つのバックエンドは、軽い自分のための場所を残さない

高くつく前提を持つ言語は、たいてい逃げ道を獲得する。より軽く動的な第二の実装が現れて、保証を手放す 代わりに到達範囲を稼ぐ、という形だ。よくあるパターンなので、ここでも使えるのか確かめる価値はある。

ほとんど使えない。本流がすでに吸収してしまっているからだ。Mere にはバックエンドが 5 つある — インタプリタ、C バックエンド、LLVM、WebAssembly、そして直接の RV32IM 機械語だ。速い起動、小さな ターゲット、ブラウザ、ベアメタル。そのすべてが兄弟プロジェクトではなく主実装の中にある。小さな Mere のための場所は見当たらない。小さな Mere がすでに同じ Mere だからだ。

もし誰かが欲しがるとして、残っているものはインタプリタではない。GC を持ち、実体化ごとの具体化ではなく 型消去を使い、ジェネリックの境界をまたいで分離コンパイルできる Mere だ。その版はプラグインを読み込め るし、どのスコープにも収まらない寿命の購読を解放できる。同時にそれは、言語の出発点にある前提を両方 捨てている — 実装と呼ぶのはごまかしになる程度に、別の言語に近い。

そこが気づいておく価値のあるところだと思う。この三つの制限はバックログではない。他をそのままにして これらを取り除く何かが、後から到着することはない。設計の穴ではなく、選択の形が外から見えているだけ だからだ。

だからこそ、否の答えを、確信が持てるまで手間をかけて書き留めたことに意味があった。仕様とは、言語が 約束することだけではない。何を断るか、そしてどれだけ早くそれを断ったかも仕様だ。11 個の watcher は 安かった。同じ否を二度目、三度目に発見するのは、そうではなかったはずだ。

← Back to Notes