trait が与えるものを、trait なしで
どの dogfood アプリケーションも同じボイラープレートを手書きした:値をどう表示するか、二つをどう比較するか、record を JSON にどう変えて、どう戻すか。そのボイラープレートこそ trait や typeclass システムが消すために存在するものだ —— だがそれを建てることは、解決、coherence、辞書渡し、明示的であることを尊ぶ言語と相容れない機構まるごとを意味する。Mere はより狭い道を取った。すでに、trait をどこにも持たず型で dispatch し、完全にコンパイル時に解決される polymorphic builtin を一つ持っていて、システムを建てる代わりにその種から一家を育てた。その一家の最も興味深いメンバは、まったく設計されなかった —— 敵対的な入力を受ける dogfood が、その正確な形を強いた。
どの dogfood アプリケーションも手で書き、また書き、書くのを嫌った類のコードがある。ログ行のために値を テキストとして描画する方法。二つの値が等しいか決める方法。record を JSON に変える方法、そしてもっと 悪いことに、JSON を record に戻す方法、フィールドごとに、rename のたびに不一致を伴って。database ライブラリは手書きの encoder と decoder で分厚かった。このボイラープレートは偶発的でない;それは まさに、trait システムと typeclass が廃絶するために発明されたものだ。だから一つ建てる圧力はしばらく 高まっていて、それに答える時が来た。
建てたくないシステム
厄介なのは、trait システムが大きく主張の強い機械だということだ。それは各使用でどの実装が当てはまるかを 解決する方法、二つの import が型の表示のしかたで不一致になれないようにする coherence の規則、そして たいてい runtime の仕掛け —— 値の傍らに見えず渡される辞書 —— を、選ばれた実装をそれが走るところへ運ぶ ために要する。そのすべてが実行可能で、そのすべてが、言語が手放すのを拒んできた一つのことに反して 引っぱる:runtime で起きることはソースから読み取れるべきで、背後で threaded される見えない引数などない、 ということ。trait の以前の紙上研究がまさにこの結論に達し、アイデアまるごとを dogfood が痛みを避け られなくするまで defer に分類していた。痛みはいま到来した。問いは、フル機械がそれと共に到来せねば ならないか、だった。
すでにそこにあった種
そうではなかった、なぜなら言語は同じ問題へのより小さな答えを、平明に隠れて、すでに含んでいたからだ。 任意の値をテキストとして描画する builtin はすでに polymorphic だった —— 整数、list、record、variant に 適用できた —— それでいてソースのどこにもそれのための trait も、instance 宣言も、辞書もなかった。それが 機能したのは、型推論器がそれの使われるあらゆる場所で具体型をすでに知っていて、コード生成器がその特定の 型のための正しい描画を、inline で、コンパイル時に emit しただけだからだ。一つの名前、多くの型固有の 振る舞い、静的に解決され生成コードに明示される。それが trait が与えるものの大半で、trait 機構の どれもなしに達成される —— そしてそれはずっと言語の中に、ありふれた builtin として座っていた。
だから一手はシステムを建てることでなく、その種から一家を育てることだった。値がコンパイル時特殊化に よってテキストとして描画できるなら、同じやり方で JSON として描画できる;構造的な JSON encoder が 一家に加わり、record を型名を落として object に、list を array に、option をその中身か null のどちらかに 変えた。構造的等価も加わり、ある型の二つの値をその形を歩いて比較した。新しいメンバはそれぞれ、新しい 機構を導入するでなく既存の特殊化 path を再利用し、それが連載全体が回る規律だ:新しい問題が現れたとき、 もっと発明する前にすでに建てた機構を拡張する。
一つの正直な非対称
encoder には自然な相棒があった —— decoder、JSON テキストを typed 値へ戻す —— そしてそれを建てることは、 正直であるに値する genuine な非対称を surface した。encoder は手渡された値を見てその値の型で dispatch できる。decoder はできない:それが手渡されるものは文字列で、文字列はそこから何を得たいと望もうと 文字列だ。だから decoder は対象の型を引数からでなく call site から、何に 向かって デコードするかを 言う annotation から取る。その型を与えられ、構造的な仕事を逆に行う —— JSON object はフィールド名を 照合して record に、array は list か tuple に、値か null は option に、タグ付きの形は variant になる。 それは、情報が違う方向から来ねばならない一箇所を除いてあらゆる点で encoder の鏡で、それを平明に言うのは 対称が完璧なふりをするより良い。
dogfood が設計したメンバ
一家の最も示唆的なメンバは紙の上で決して設計されなかった。decoder は、最初に建てられたとき、fail-fast だった:不正な入力を手渡すと abort する —— interpreter では例外、native バイナリではプロセスが exit する。それは trusted な入力にとって正しい振る舞いだ:設定ファイル、内部データファイル、テスト fixture、 そこでは不正入力は大声で聞きたいバグだ。それから blog アプリケーションが、決して信頼してはならない 唯一のもの —— 到来する web リクエストの body —— にそれを使おうとした。敵対的な入力に対する fail-fast な decoder はバグを報告しない;一つの不正なリクエストでサーバまるごとを倒す。それは security の退行で、 便利さでない。
だから二つ目の decoder が存在を強いられた、abort する代わりに optional な値 —— 失敗時は無 —— を返す もの。それはどのロードマップにもなかった;dogfood が、実のネットワークに晒された実のサーバであることに よって、それを要求した。そしてそれは、いまや言語の助言の一部である明快な線を引いた:abort する decoder は信頼する入力のため、optional なほうは信頼しない入力のため。blog の signup と login と投稿が typed な リクエスト record を optional decoder に通す形に書き直され、修正はそれが重要なところで検証された —— 正常な body は作成されたリソースを生み、不正なものはクライアントエラーを生み、そして最も重要なことに、 サーバはその後もまだ生きていて次のリクエストに正しく応える。loop は実の database 相手に、native バイナリ で、gap を露呈したまさにその条件の下で閉じた。
縁を、平明に
設計されるでなく育てられた一家には縁があり、有用なのはそれらを隠すでなく名指すことだ。option を裸の値 か null として encode するのは idiomatic で綺麗に round-trip するが、不在の値と、たまたま空の存在する 値とを区別できない —— 実用上稀で、それでいて実の nested-option の隅。decoder は record の全フィールドが 存在することを要するので、真に optional なキーはまだ表現できず、将来の finding として分類される。そして 一家は一度に全 backend に着地しなかった:decoder は interpreter と native path に先に行き、それを動機 づけたまさにそのアプリケーションのブラウザデプロイを一時的に壊し、WebAssembly 版が次のリリースでその 負債を返し backend を水平にするために続いた。これらのどれも feature の記述のしかたの中で隠されていない、 なぜなら保証はその述べられた限界が値付けするものだけの値だからだ。
アークまるごとが買ったのは trait システムが売る便利さ —— もう手書きの printer も、comparator も、 encoder も、decoder もない —— を trait システム自身なしで:解決なし、coherence の規則なし、辞書なし、 プログラムを通して見えず threaded されるものなし。それは、一つの既存 builtin がすでに難しい部分を 解いていたと気づき、その似姿で一家を育てることから来た;そしてその最も鋭い feature は、設計者の先見 からでなく、開けた場所に立って撃たれた dogfood から来た。