Writing Decision Rules

Learn how to express an application decision as a Lispex rule. Write and test a refund approval and a discount rate rule.

This is an earlier edition of the documentation. To write and test a refund rule, start with the current First Decision Rule. To save and check its execution record, follow Run and Check Decision Records.

Not every piece of code is a decision rule. This guide starts with how to choose what belongs in one, then walks through writing and testing two examples, a refund approval and a discount rate. Every rule here stays inside the checked subset used by Lispex Vouch, so the same input always gives the same answer.

What belongs in a rule

Three questions pick the candidates.

  • Might someone ask you to explain this judgement later? Refund refusals, discount rates, and approval limits attract support tickets and audits, which makes them good candidates.
  • Can you list every input the rule reads? If the judgement comes down to a few values, like days since delivery and whether the package was opened, it can be a rule. Values the rule would have to look up itself, like the current time or live inventory, must be read by the host first and passed in as arguments.
  • Does it produce an answer without touching a screen or a store? A rule only judges. What to do with the judgement stays in the host application.

Three yeses make a good candidate. If one answer is no, leave that part in the app and cut the judgement smaller.

The shape of a rule function

A rule is one function that takes its inputs as arguments and returns the answer. A few habits keep rules readable.

  • Name a yes-or-no rule with a trailing question mark, like refund-auto-approved?.
  • Name a value-producing rule after its answer, like discount-rate.
  • Take every input as an argument. Read nothing outside the rule and change nothing inside it.

Example 1. Refund approval

A refund is approved automatically within fourteen days of delivery, unopened.

LISPEX
(define (refund-auto-approved? days-since-delivery opened?)
  (and (<= days-since-delivery 14)
       (not opened?)))

(refund-auto-approved? 9 #f)   ; ⇒ #t
(refund-auto-approved? 14 #f)  ; ⇒ #t  the boundary day still approves
(refund-auto-approved? 15 #f)  ; ⇒ #f  one day late
(refund-auto-approved? 3 #t)   ; ⇒ #f  opened package

Keep the test calls right under the rule and its edges become documentation. Is day fourteen approved? The code answers directly.

Example 2. Discount rate

cond suits judgements that branch several ways. Say the discount rate depends on membership level and cart total. Levels are numbers, 2 for gold, 1 for silver, 0 for regular.

LISPEX
(define (discount-rate member-level cart-total)
  (cond ((and (= member-level 2) (>= cart-total 100000)) 15)
        ((= member-level 2) 10)
        ((>= cart-total 100000) 5)
        (else 0)))

(discount-rate 2 120000)  ; ⇒ 15  gold, large cart
(discount-rate 2 50000)   ; ⇒ 10
(discount-rate 0 120000)  ; ⇒ 5
(discount-rate 0 30000)   ; ⇒ 0

cond checks its clauses from the top and answers with the first one that holds. Order is priority, so the most specific condition goes first.

Testing a rule

There are two ways, and the same reference interpreter evaluates both.

Paste the rule and its test calls into the Playground and the results appear immediately. There is nothing to install, which suits the shaping stage.

To keep rules in files, save them as something like rules.lspx and run the CLI.

BASH
npm install -g lispex
lispex run rules.lspx

Writing the expected value next to each test call as a comment lets you follow, by eye, exactly what a later change to the rule moves.

Staying inside the checked subset

The examples above use define, and, cond, and comparison operators, which already keeps them inside the subset. As rules grow, two things keep them there.

  • No macros and no reader extensions. In the checked subset they are immediate errors. The full list of allowed forms is in Syntax.
  • Use only the arguments the host passed in. A rule that fetches the time, random numbers, or external data on its own breaks the promise that the same input gives the same answer.

Receipts come next

The rules written in this guide work directly with Lispex Vouch. When a rule drives a real decision, generate a receipt with the native binary and store it alongside the decision record. The receipt records the rule, datum, and result. The host application records the acting identity, action time, and deployed rule under its own policy. The lispex verify and lispex replay commands are described on the Lispex Vouch page.