records で領域を書く
これまでの probe はどれもアルゴリズムかバイナリ形式で、唯一測っていなかった軸が、業務領域を record でモデリングする書き味だった。money・account・entry・transaction の record 型とたくさんの入れ子更新を持つ複式簿記の台帳はよいストレステストで、平明に隠れていた parser の穴を炙り出した。Mere は小文字の型名と大文字のコンストラクタという ML 慣例に従い、record 宣言もそれを守っていた —— だが record リテラルは型名が大文字のときだけ parse され、小文字だと変数 + block に落ちて原因から遠いエラーで失敗した。修正は 1 分岐 —— 登録済みの record 名(大小問わず)に波括弧が続けば record リテラルだ。それ以降、領域は綺麗にモデリングでき、正直な粗が一つ残った。
probe はどれも同じ方に傾いていた —— アルゴリズム、数値カーネル、バイナリ形式 —— そして一度も 触れなかったのが、たいていのソフトウェアが実際そうである最も平明な種類のプログラム、record で領域を モデリングすることだった。凝ったデータ構造でなく、意味のあるフィールドを持つ record 型を一握り、 構築し、更新し、読む。複式簿記の台帳はその形の凝縮版だ。money、account、entry、transaction が各々 record で、postings が残高を動かすにつれ入れ子の更新が起きる。それを書くのが probe で、要点は動くか どうかより、どう感じるか —— そしてどこで擦れるか —— だった。
ML 慣例の穴
擦れたのは即座に、parser で、だった。Mere は型名が小文字でコンストラクタが大文字という ML 伝統に
従う —— type 'a list = Nil | Cons of ...。そう宣言された record 型は文句なく受理される。
type addr = { city: str } は有効な宣言だ。だが record リテラル —— addr { city = "kyoto" } ——
は型名が大文字で始まるときだけ parse された。波括弧の頭の小文字名は一般ケースに落ち、変数に続く
block として読まれ、「expected ‘;’ or ‘}’ in block」で失敗した —— block 構文についてのエラーで、
実際の問題のどこも指していない。問題は、parser が小文字の record コンストラクタを認識しないこと
だった。variant・list・option は小文字の型名で常に動いていた。record だけが慣例に穴があった。
record リテラルの判定が先頭大文字を検査していたからだ。
一つの分岐
修正は小さく、誤 parse の起きるまさにその場所にあった。「小文字名は変数」に落ちる前に、その名前が
登録済みの record 型か(大小問わず)を確認し、次のトークンが開き波括弧なら record リテラルを parse
する。その分岐を入れると、record の語彙一式が大文字小文字によらず動いた —— 構築、フィールドアクセス、
入れ子更新、一つの深いフィールドだけを変えて record を組み直す、あの見た目の厄介な
{ p | home = { p.home | city = ... } } を含めて。修正が一つの分岐だったことが、設計が正しく
parser だけが遅れていた証しだ。型システムも意味論も動かす必要はなく、ただ一箇所で小文字名を認識する
だけだった。
残った粗
一つの粗は磨かれなかった。それは他所でも現れる同じ規則で、本当はバグではないからだ。更新される record パラメータは型を注釈せねばならない。注釈がないと、関数はそのパラメータについて多相で、更新の場所には 更新すべき record 型がない —— エラーは「record update base must be a record value」だ。これは numeric オーバーロードが求めるのと同じ要求で、算術に流れ込む多相パラメータは、演算子がどの型を扱って いるか分かるよう注釈されねばならない。base が具体的なフィールドアクセスの入れ子更新は問題ない。裸の 多相パラメータだけがヒントを要る。それを直さず立ったままの粗として記録するのは意図的な選択だ —— 回避は注釈一つ、真の修正はより大きな推論の変更で、pain がまだそれを正当化しない。台帳は釣り合い、 revenue が cash と 1 の位まで一致し、小文字の穴が閉じた今、言語の record 側はよく読める。