← Blog

Kumi:RubyとJavaScriptの両方で動く計算ロジック

私が手がけるプロジェクトには、ウェブショップや請求システムがたくさんあります。そこで毎回出てくるのが同じ問題です。料金の計算ロジックはサーバー側にありますが、お客さんは数量を変えながらブラウザで合計金額を見たいのです。そのためルールをRubyとJavaScriptで二度書くことになり、いずれ二つの計算結果が食い違います。

André MutaさんのKumiは、この問題に正面から取り組むRubyのgemです。税金のルールや料金、スコアのような計算ロジックのための宣言的なDSLで、ひとつのスキーマから素のRubyコードとJavaScriptコードを生成します。実際に試してみて、とても気に入りました。

Kumiでできること

入力データの形と、計算したい値を書くだけです。ループや計算の順番は自分で書きません。コンパイラが依存関係を調べて型をチェックし、それぞれの言語向けに、依存ライブラリのないシンプルなコードを生成します。

普通のRubyメソッドと違う点は、主に二つあります。

インストールはこちらです。

gem install kumi

Ruby 3.1以降が必要で、ライセンスはMITです。現在のバージョンは2026年6月の0.1.0です。READMEではプロジェクトは実験段階とされていて、公開APIは今後変わる可能性があります。

例:スイスの請求書

試しに、小さな請求書を書いてみました。10個以上の明細には10%のまとめ買い割引をつけ、スイスの付加価値税8.1%を加え、合計はスイスの現金払いで一般的な5ラッペン単位に丸めます。

require "kumi"

module Invoice
  extend Kumi::Schema

  schema do
    input do
      array :items do
        hash :item do
          integer :quantity
          decimal :unit_price
        end
      end
    end

    trait :bulk, input.items.item.quantity >= 10

    let :gross, input.items.item.quantity * input.items.item.unit_price
    value :line_totals, select(bulk, gross * 0.9, gross)
    value :subtotal, fn(:sum, line_totals)
    value :vat, subtotal * 0.081
    # no round() yet: scale to 5 Rappen, add 0.5, truncate
    value :total, fn(:to_integer, (subtotal + vat) * 20 + 0.5) / 20.0
  end
end

result = Invoice.from(items: [
  { quantity: 2,  unit_price: 50.0 },
  { quantity: 12, unit_price: 10.0 }
])

result[:line_totals] # => [100.0, 108.0]
result[:subtotal]    # => 208.0
result[:vat]         # => 16.848
result[:total]       # => 224.85

ポイントをいくつか挙げます。

同じロジックをJavaScriptで

ウェブショップにとって一番おもしろいのはここです。一回の呼び出しでJavaScriptのモジュールが書き出されます。

Invoice.write_source("invoice.mjs", platform: :javascript)

value、let、trait はそれぞれエクスポートされた関数になります。生成された _total をそのまま載せます。

export function _total(input) {
  let t88 = input["items"];
  let acc95 = 0;
  for (let items_i90 = 0; items_i90 < t88.length; items_i90++) {
    let items_el89 = t88[items_i90];
    let t91 = items_el89["quantity"];
    let t92 = 10;
    let t41 = t91 >= t92;
    let t93 = items_el89["unit_price"];
    let t48 = t91 * t93;
    let t94 = 0.9;
    let t51 = t48 * t94;
    let t59 = t41 ? t51 : t48;
    acc95 += t59;
  }
  let t96 = 0.081;
  let t87 = acc95 * t96;
  let t28 = acc95 + t87;
  let t97 = 20;
  let t30 = t28 * t97;
  let t98 = 0.5;
  let t32 = t30 + t98;
  let t33 = __core_to_integer(t32);
  let t99 = 20.0;
  let t35 = t33 / t99;
  return t35;
}

生成コードに求めたいのは、まさにこういう形です。ひとつのループの中で割引・合計・付加価値税・丸めまで済ませていて、裏で動くライブラリもインタプリタもありません。読んで理解でき、デバッガで一行ずつ追え、そのまま配布できます。同じデータでNode.jsから実行したところ、Rubyとまったく同じ [100, 108]、208、16.848、224.85 という結果になりました。

ruby.wasmでのKumi

私はRuby Koans in the Browserなどでruby.wasmをよく使っているので、KumiそのものがWebAssemblyにコンパイルされたRubyの中でも動くのか試してみました。数行の準備をすれば、ちゃんと動きます。

# before require "kumi"
ENV["KUMI_PASS_BUDGET_MS"] = "0"   # no threads in WebAssembly: no compile time limit
require "tmpdir"
Dir.mkdir("/tmp") unless Dir.exist?("/tmp")
def Dir.tmpdir = "/tmp"            # a place for Kumi's code cache
require "kumi"

Kumiはコンパイラの各パスに Timeout を使った時間制限をかけていますが、これはスレッドを起動し、ruby.wasmにはスレッドがありません。環境変数でこの制限をオフにします。また、Kumiは生成したRubyコードをキャッシュ用のディレクトリに書き出してから読み込みますが、ブラウザのインメモリのファイルシステムでは、Rubyが自分で一時ディレクトリを見つけられません。

この準備をすれば、上の請求書の例はruby.wasmでもまったく同じ 224.85 という結果になります。ruby.wasm 2.10.1(Ruby 4.0)をNode.jsで動かし、ruby.wasmがブラウザで使うのと同じインメモリのファイルシステムで試しました。Kumiは純粋なRubyで書かれていて、依存しているzeitwerkとmutex_mもそのまま読み込めます。

つまり、ルールをブラウザに持っていく方法は二つあります。いちばん軽いのはJavaScriptへのエクスポートです。フロントエンドですでにRubyをWebAssemblyで動かしているなら、その場でKumiのスキーマを定義して実行することもできます。

ミスを早めに見つける

Kumiはスキーマを定義した時点で条件を解析します。年齢が0から150の範囲だと宣言したうえで、決して真にならない条件を書くと、スキーマの読み込みが失敗します。

schema do
  input { integer :age, domain: 0..150 }
  trait :impossible_age, input.age == 200
end
# Kumi::Core::Errors::SemanticError: conjunction `impossible_age` is impossible

業務ルールでこういう条件があるのは、たいてい打ち間違いか要件の誤解です。何か月も後になって割引が一度も適用されていなかったと気づくのではなく、Kumiはコードを読み込んだ瞬間に教えてくれます。これだけでも試す価値があります。

ルールを組み合わせる

import :tax, from: TaxPolicy のように、スキーマから別のスキーマを取り込めます。ひとつの金額しか知らないルールを注文の各明細に適用でき、コンパイラはそれを生成コードに直接埋め込みます。詳しくはスキーマのインポートに関するドキュメント(英語)をご覧ください。

知っておくと便利なこと

ブラウザで試す

Kumiの感じをつかむには、インタラクティブなプレイグラウンドが一番の近道です。言語の入門のほか、アメリカの税金計算や1万人分の給与計算のような大きめの例もあります。

Kumiは、同じ計算をサーバーとブラウザの両方で動かしたい場面にぴったりです。料金計算、見積もりの設定画面、送料の計算など。ひとつのスキーマ、二つの言語、同じ結果。自分のプロジェクトで使うのが楽しみですし、これからの発展にも期待しています。

リンク