Table of Contents

Scoped params

When the same computed value appears in several places, name it once. A params object declares named subexpressions — on the rule set for every rule, or on one rule for itself — and { "param": "…" } references one anywhere an expression is valid. Params are still pure data: they never change what a rule can compute, only how it reads.

Declaring and referencing

{
  "name": "Scoped params",
  "params": {
    "averageLine": {
      "op": "divide",
      "operands": [ { "field": "Order.Total" }, { "field": "Order.ItemCount" } ]
    },
    "rate": 0.1
  },
  "rules": [
    {
      "id": "pricey-basket",
      "condition": {
        "expression": { "param": "averageLine" },
        "operator": "GreaterThan",
        "value": 50
      },
      "actions": [
        { "type": "setOutput", "target": "Segment", "value": "premium" },
        { "type": "setOutput", "target": "AverageLinePrice", "value": { "param": "averageLine" } }
      ]
    },
    {
      "id": "standard-discount",
      "condition": { "field": "Order.Total", "operator": "GreaterThanOrEqual", "value": 100 },
      "actions": [
        {
          "type": "setOutput",
          "target": "Discount",
          "value": {
            "op": "multiply",
            "operands": [ { "field": "Order.Total" }, { "param": "rate" } ]
          }
        }
      ]
    },
    {
      "id": "vip-discount",
      "params": { "rate": 0.2 },
      "condition": { "field": "Customer.IsVip", "operator": "Equals", "value": true },
      "actions": [
        {
          "type": "setOutput",
          "target": "VipDiscount",
          "value": {
            "op": "multiply",
            "operands": [ { "field": "Order.Total" }, { "param": "rate" } ]
          }
        }
      ]
    }
  ]
}

Three things to see in that document:

  • averageLine is defined once and used twice in pricey-basket: as the condition's computed left-hand side, and as an action value.
  • A param can be a bare constant. rate names the number 0.1, which gives the document one place to change it.
  • A rule's params shadow the set's. vip-discount redefines rate as 0.2 for itself; every other rule still sees 0.1.
using RuleWright.Core;
using RuleWright.Execution;
using RuleWright.Json.SystemText;

LoadedRuleSet rules = engine.LoadRuleSet(File.ReadAllText("scoped-params.json"));

foreach (Checkout checkout in new[] { Facts.Vip(), Facts.Newcomer() })
{
    RuleEvaluationResult result = engine.Evaluate(rules, checkout);

    Console.WriteLine($"{checkout.Customer.Name}:");
    foreach (KeyValuePair<string, object?> output in result.Outputs)
    {
        Console.WriteLine($"  {output.Key,-17} = {output.Value ?? "null"}");
    }
}

Output

Aroha:
  Segment           = premium
  AverageLinePrice  = 60
  Discount          = 24.0
  VipDiscount       = 48.0
Ben:

Where a reference is valid

A { "param": "…" } node can stand wherever an expression can: an action's value, a condition's expression, an operand inside another expression, a decision-table then cell, and inside another param's definition. Definitions may reference sibling and outer params in any order — but never in a cycle, which validation rejects with a JSON pointer at the offending definition.

One place is deliberately off-limits: inside a quantifier's per-element condition. A param's definition reads the root fact, while field paths there resolve against the element, so a reference would silently answer null. The validator rejects it instead.

Inlined at load

References are substituted when the document loads, exactly as a decision table expands into rules. The engine, the content hash and both execution paths never see a param: a param-authored document compiles to the same delegates — and the same cache entries — as its hand-inlined twin. Two consequences:

  • Params are free. There is no per-evaluation lookup; a reference costs exactly what writing the expression inline costs.
  • In C#, a param is just a shared instance. The domain model has no param node: reuse one immutable ValueExpression in several rules and you have written the same thing the parser produces. Build rules in C# shows the model.
Note

Unlike a variable, a param is not computed once per evaluation — each reference evaluates the expression where it stands. Expressions are pure and cheap, so the answers are identical; only very hot paths would notice, and a custom value function can memoize if one ever does.

Next

Collections, or the params-heavy alternative: a decision table.