cli の設計
cli パッケージは橋渡し役です。Python のインデックスビルダーや MoonBit 以外のプログラムが MoonBit のスコア規則を評価できるようにします。このページではその橋渡しの形を説明します。スコア規則そのものは score の設計 で導出しています。
設計目標
スコア、ランク、モメンタムの規則の実装はちょうど 1 つでなければならず、Web アプリケーションが配信するデータベースもその実装で計算されなければなりません。インデックスビルダーは SQLite、ファイルシステム、HTTP を扱うため Python で書かれ、規則は MoonBit ユーザーがライブラリとして呼べるよう MoonBit で書かれています。橋渡しは、規則を複製せずに、できるだけ少ない仕組みで両者をつなぐ必要があります。
数学的背景
このコマンドは 8 つの整数の関数
すなわち compute_score_snapshot を計算します。入力は JSON で届くため、コマンドはまず JSON 値から への全域的なデコード写像 を適用し、 を出力します。各キー について、
ここで はゼロ方向への丸め、 は への切り詰めです。 は全域的なので、失敗はその外側、つまり引数の誤り、読み込めないファイル、JSON でないテキストに限られます。スコアは負のカウントを に切り上げるので、合成 はあらゆる JSON 値をスナップショットに写します。
ここから次の 2 つの性質が従い、呼び出し側はこれに依存しています。
- 決定性。 と は純粋なので、同じファイルからは常にバイト単位で同じ出力が得られます。
Json::stringifyはフィールドを宣言順に書き出し、JavaScript ターゲットでは各Doubleを往復可能な最短形式で出力します。 - ライブラリとの一致。 値が
Intの範囲内の整数である入力オブジェクトでは、 はそれらの値に対して恒等写像なので、コマンドの出力はちょうどJson(@score.compute_score_snapshot(...))になります。
設計上の判断
スナップショットごとに 1 プロセス、ファイル経由
課題。 Python から MoonBit のコードを呼び出す必要があります。
選択肢。 式を Python で実装し直す、WebAssembly にコンパイルした MoonBit を Python から呼ぶ、常駐する MoonBit サーバーを動かす、スナップショットごとに MoonBit プログラムを実行する。
採用。 Python がパッケージごとに 1 回起動する JavaScript の実行可能プログラムで、入力は一時ファイルで渡します。Python 版の式は 2 つ目の正となる実装になってしまいます(使われていないものが scripts/build_index.py にまだあります)。WebAssembly のホストやサーバーは、インデックスのビルドごとに 1 回しか走らない処理のために依存関係やプロトコルを増やします。ファイルを使えばコマンドラインが短く済み、ビルダーは finally ブロックで入力を削除できます。
代償はパッケージごとの Node.js の起動です。プロセスの起動には数十ミリ秒かかる一方、スコアの計算は数マイクロ秒なので、 個のパッケージに対するビルダーの評価段階は 回のプロセス起動になります。数千パッケージ規模のレジストリなら、オフラインのビルドとして許容範囲です。
JavaScript 専用
コマンドは extern "js" 関数を通じて fs.readFileSync でファイルを読み、process.exit で終了するため、パッケージは supported_targets = "js" を設定しています。リポジトリは Web アプリケーションのためにいずれにせよ Node.js を必要とするので、ほかのランタイムは要りません。
入力は寛容に、エラーは厳格に
課題。 呼び出し側がすべてのシグナルを知っているとは限りません。たとえばビルダーがダウンロード数を取得できなかったパッケージです。
採用。 欠落したフィールドと数値でないフィールドは、どのシグナルにとっても中立な値である 0 にデコードします。不正な JSON や誤った引数はデータの欠落ではなく呼び出し側のバグを示すので、error オブジェクトを出力してステータス 1 で終了します。エラーオブジェクトが JSON なので、呼び出し側はどの場合でも標準出力を解析できます。
ラベルは MoonBit で計算する
コマンドは数値だけでなくラベルも返します。ビルダーが保存済みのスコアから rank_label を自前で計算すると、MoonBit でしきい値を変えてもデータベースには黙って反映されません。スナップショット全体を返すことでラベルと数値が 1 回の評価から得られるため、スナップショットの一貫性 という不変条件がデータベースでも成り立ちます。
正しさと不変条件
- 終了ステータス
0は、標準出力がScoreSnapshotの 7 つのフィールドを持つ JSON オブジェクト 1 行であることを意味します。 - 終了ステータスが
1で標準出力が JSON オブジェクトなら、それはerrorキーを持ちます。入力ファイルを読み込めない場合も1で終了しますが、書き出すのは標準エラーだけです。 - 出力は現在時刻、環境、入力以外のファイルに依存しません。時刻に関わるシグナルはすべて呼び出し側が計算します。
採用しなかった案
- 標準入力から読む。 一時ファイルは不要になりますが、パスに対する
readFileSyncは Node.js が対応するどのプラットフォームでも同じように動き、ビルドがうまくいかないときにファイルを調べることもできます。 - バッチモードで 1 プロセスあたり多数のパッケージを評価すれば、パッケージごとの起動コストはなくなります。これは実装されていません。パッケージごとの約束のほうが単純で、現在のレジストリ規模には十分な速さです。
- シグナルごとのコマンドラインフラグ(
--dependents 20)は呼び出しを長くし、パーサーも必要になります。JSON ならビルダーがすでに持っている辞書とそのまま対応します。
境界
- コマンドが評価するのは 1 つのスナップショットだけです。レジストリの読み込み、SQLite への問い合わせ、ダウンロード数の取得、日付の計算は行いません。
- シグナル同士の整合性は検証しません。
- 読み込めないファイルは JSON ではなく Node.js を通じて報告されます。
- JavaScript ターゲットと Node.js でのみ動作します。