はじめに
技術ブログのネタとして、JsonLogicを利用した柔軟なシステムカスタマイズについて考えてはいたのですが、もう一捻り欲しいなというところで、最近話題のChatGPTと組み合わせたらどうだろうと思い試してみたところ、思いのほかいい感じの結果が得られたので、タイトルを改めて書いてみました。
JsonLogicについて
JsonLogicは汎用的なルールエンジンで、現在、NearMeのサービスにおいて複雑な料金計算等で利用しています。例えば、ユーザーに提示する料金は、需給のバランスを整えるために、時間帯や配車までの時間等で金額を調整することがあります。また、配車コストを算出するために、タクシー会社と契約する料金表があるのですが、これも、エリアや人数(大人、子供)、荷物の数、時間などの変数が複雑に絡んだものがあります。これらを計算するために、一つはハードコーディングして解決するという手もありますが、更新しにくく、スケールもしにくいです。また、DB上に設定パラメタを持たせて計算するという手もありますが、パラメタ数が多すぎたり、ロジックも拡張性を持たせるのが難しくなったりします。JsonLogicを用いるとそれらをうまく解決できます。
さて、JsonLogicについてChatGPTに聞いてみます。
- JsonLogicとは
JsonLogicは、JSON形式で表現されたルールエンジンであり、データを扱うための簡単な文法を提供します。JsonLogicを使用すると、単純な条件分岐や数式計算、データフィルタリングなどのタスクを簡単に実行できます。 JsonLogicは、JavaScriptオブジェクトのような簡単な構造を使用してルールを定義します。ルールは、if/elseステートメントのような基本的な条件分岐、算術演算、文字列操作、配列の操作、そしてネストされたルールの組み合わせをサポートします。 JsonLogicは、特にビジネスルールやデータ処理の自動化などの分野でよく使われます。JsonLogicのシンプルな構文と柔軟性により、データ処理やビジネスロジックの自動化を効率的かつスケーラブルに実行することができます。
うまく簡潔に説明されていますね。補足として、JsonLogicは、ルール内において、変数の代入や、ループ、関数定義、GOTO文を許しておらず、副作用がなく、計算時間も発散しないように作られています。ただし、明示的に宣言すれば、独自に定義した関数を呼び出すことができるので、やりたいことは大体できるようになっています。
実際にどのようなものか例を示したいと思います。
> jsonLogic.apply({ "+" : [1, 1] }) 2
ここで、applyの引数の部分は"1 + 1"というルールを表していて、applyによって計算が行われて2を出力しています。
ルールをネストすることも可能です。
> jsonLogic.apply({ "*": [{ "+" : [1, 1] }, 3] }) 6
これは、"(1 + 1) * 3"を表しています。
変数は次のようにして定義します。
> jsonLogic.apply({ "+" : [{ "var" : "x" }, { "var" : "y" }] }, { x: 1, y: 2 }) 3
ここで、ルールは"x + y"を表していて、xに1、yに2を代入して、3を出力しています。
サポートされている演算子は、四則演算のみならず、If文や比較演算、論理演算、配列操作などがあります(参考)。
さらに、独自に定義した関数も次のようにして利用することができます(参考)。
> jsonLogic.add_operation("sqrt", Math.sqrt); > jsonLogic.apply({ "sqrt":2 }) 1.4142135623730951
ChatGPTでJsonLogicを生成
それではChatGPTがJsonLogicを扱えるか問い合わせてみます。
試行時のChatGPTのバージョンは"ChatGPT Mar 23 Version" (ChatGPT Plus未加入)です。
- 1+1をJsonLogicで表現してください
以下は1+1をJsonLogicで表現した例です。 { "+" : [1, 1] } この式は、+演算子を使用して2つの数値1を加算することを意味します。JsonLogicは、JavaScriptの構文に似ていますが、JavaScriptのように括弧を使用する必要がないため、式が簡潔で直感的になります。
おぉ!ちゃんとJsonLogicの式を返してくれました。JsonLogicというネット上で比較的ニッチな文法を理解してるのが驚きです。
ではもっと現実的な課題を提示してみます。
- 以下をJsonLogicで表現してください。時間に応じて、通常料金3000円、深夜料金4000円 を出力します。深夜料金は朝5時までまたは夜22時以降に適用されます。時間はhourの変数を用いてください。
以下は、JsonLogicを使用して時間に応じて通常料金と深夜料金を出力するロジックを表現した例です。 { "if": [ { ">=": [ { "var": "hour" }, 22 ] }, 4000, { "if": [ { "<": [ { "var": "hour" }, 5 ] }, 4000, 3000 ] } ] } このロジックでは、まず現在の時間(hour)が22時以上であれば、深夜料金(4000円)が適用されます。そうでなければ、時間が5時未満であれば深夜料金(4000円)が適用され、それ以外の場合は通常料金(3000円)が適用されます。
すごいですね。"if"文も"var"の変数も理解していました。しかも、ちゃんと「朝5時まで」を"hourの5以下"ではなく"hourの5未満"として処理してくれました。
ただし実はこの結果を得る前にいくつか試行錯誤はありました。 確率的に構文を間違えることもあり、説明を丁寧にしないとその確率が増える感じでした。 また、説明の仕方によっては深夜料金を増加分として解釈し3000 + 4000を返すこともありました。
続いて、もう少し出力を改良してみます。
- "if"のネストを、"or"演算子を用いて解消してください。
以下は、"if"のネストを"or"演算子を使用して解消した例です。 { "if": [ { "or": [ { ">=": [ { "var": "hour" }, 22 ] }, { "<": [ { "var": "hour" }, 5 ] } ] }, 4000, 3000 ] } このロジックでは、現在の時間が22時以上であるか、5時未満であるかどうかを"or"演算子で判定しています。もし、そのいずれかであれば深夜料金(4000円)が適用され、そうでなければ通常料金(3000円)が適用されます。
望んだ結果が得られました。ネストした"if"の関係性から、"推論"的な感じで、"or"条件に還元することができました。
テストコードも依頼してみます。
- JavaScriptでテストコードをテストケースとともに生成してください。JsonLogicの式はテストケースから外出しして共通化してください。
以下は、JavaScriptでJsonLogic式を共通化し、テストコードとテストケースを生成する例です。 const JsonLogic = require('json-logic-js'); // 共通化したJsonLogic式 const pricingRule = { "if": [ { "or": [ { ">=": [ { "var": "hour" }, 22 ] }, { "<": [ { "var": "hour" }, 5 ] } ] }, 4000, 3000 ] }; // テストケースを生成する関数 function generateTestCase(hour, expectedPrice) { return { input: { hour }, expectedOutput: expectedPrice }; } // テストコードを生成する関数 function generateTestCode(pricingRule) { const testCases = [ generateTestCase(0, 4000), generateTestCase(3, 4000), generateTestCase(5, 3000), generateTestCase(10, 3000), generateTestCase(20, 3000), generateTestCase(22, 4000), generateTestCase(23, 4000), ]; testCases.forEach(({ input, expectedOutput }) => { const actualOutput = JsonLogic.apply(pricingRule, input); console.log(`Input: ${JSON.stringify(input)}, Expected Output: ${expectedOutput}, Actual Output: ${actualOutput}`); console.assert(actualOutput === expectedOutput, `Test Failed: ${JSON.stringify(input)}`); }); } // テストコードを実行する generateTestCode(pricingRule);
想像以上にいい感じのテストコードが返ってきました。境界値周りも考慮されています。
ChatGPTでJsonLogicを翻訳
今度は、先ほどのJsonLogic式を逆に変換して日本語に直してみます。
- 次のJsonLogic式を日本語で表現してください。 {"if":[{"or":[{">=":[{"var":"hour"},22]},{"<":[{"var":"hour"},5]}]},4000,3000]} ただし、出力は料金を表しています。
このJsonLogic式は、次のように日本語で表現できます。 「もし現在の時間が22時以上であるか、5時未満である場合は、料金を4000円に設定し、それ以外の場合は料金を3000円に設定する。」 ここで、「var」で参照されている「hour」は、式が実行される瞬間の時間を表す変数として想定されます。また、「4000」と「3000」は、それぞれの場合に適用される料金を表しています。
正しく変換されました。
もっと複雑な例を試してみます。
- 次のJsonLogic式を日本語で表現してください。{"if":[{"and":[{"in":[{"var":"from_area"},["area1","area2","area3"]]},{"in":[{"var":"to_area"},["itami"]]}]},3500,{"and":[{"in":[{"var":"from_area"},["area1","area2","area3"]]},{"in":[{"var":"to_area"},["kanku"]]}]},4500,{"and":[{"in":[{"var":"from_area"},["area4","area5","area6"]]},{"in":[{"var":"to_area"},["itami"]]}]},4000,{"and":[{"in":[{"var":"from_area"},["area4","area5","area6"]]},{"in":[{"var":"to_area"},["kanku"]]}]},5000,3000]} ただし、出力は料金を表しています。
このJsonLogic式は、次のように日本語で表現できます。 「もし出発地がarea1、area2、またはarea3であり、到着地がitamiである場合は、料金を3500円に設定する。出発地がarea1、area2、またはarea3であり、到着地がkankuである場合は、料金を4500円に設定する。出発地がarea4、area5、またはarea6であり、到着地がitamiである場合は、料金を4000円に設定する。出発地がarea4、area5、またはarea6であり、到着地がkankuである場合は、料金を5000円に設定する。上記の条件がどれにも当てはまらない場合は、料金を3000円に設定する。」 ここで、「var」で参照されている「from_area」と「to_area」は、それぞれ出発地と到着地を表す変数として想定されます。また、各エリアに対する料金は、条件分岐によって異なります。
読みやすくなりました。変換も正しいです。
おわりに
ChatGPTがJsonLogicを正しく理解して扱えてることを示してきました。現状、説明が雑だったり曖昧だったりすると意図した結果が得られないことがありますが、十分活用できるレベルだと思います。今後さらに精度が高くなることも期待できます。
JsonLogicはシステムに高い柔軟性をもたらしますが、それでいて比較的安全でシステム側としては扱いやすいです。難点としては、文法を覚えないといけず、書きづらい読みづらいというところはあったのですが、ChatGPTで翻訳すればそれも解消しそうです。将来的には、例えば、特定の条件の時に何らかのアクションを実行するというタスクを、ChatGPTに音声で指示して実行させたいといった場合、ChatGPTがJsonLogicを介してシステムを制御するということもあり得そうです。
最後になりますが、NearMeではエンジニアを募集しています!ご興味のある方はぜひ以下をご覧ください。
Author: Kenji Hosoda