Search code examples
clojure

How to implement a decision matrix/table in Clojure


There is a requirement to implement a decision table as below:

MemberType       Amount      => Discount
"Guest"          > 2000      => 3%
"Silver"         any         => 5%
"Silver"         > 1000      => 10%
"Gold"           any         => 15%
"Gold"           > 500       => 20%

I would imagine, if properly implemented in Clojure, we can define a rule table as below:

(defrule calc-discount
  [member-type   amount] 
  "Guest"        (greater-than 2000) => 0.03
  "Silver"       (anything)          => 0.05
  "Silver"       (greater-than 1000) => 0.1
  "Gold"         (anything)          => 0.15
  "Gold"         (greater-than 500)  => 0.2
 )

Of course, there should be a better way in writing/defining such a rule set. Yet the key thing I think is how to define "defrule" to make this happen?


Solution

  • For this example, you can express the business logic rather concisely with condp.

    (defn discount 
      [member-type amount] 
      (condp (fn [[type tier] _] (and (= member-type type) (> amount tier))) nil 
        ["Guest"  2000] 0.03 
        ["Silver" 1000] 0.10 
        ["Silver"    0] 0.05 
        ["Gold"    500] 0.20
        ["Gold"      0] 0.15 
        0.00))
    
    (discount "Gold" 600) ;=> 0.2
    

    If you are looking to implement the syntax as in your example, you'll need to write a macro. A very rough example:

    (defmacro defrule [name fields & clauses]
      (let [exp (fn [f c] (if (list? c) (list* (first c) f (rest c)) (list `= c f)))]
        `(defn ~name ~fields 
           (cond 
             ~@(for [clause (partition-all (+ 2 (count fields)) clauses)
                     form [(cons `and (map exp fields clause)) (last clause)]] 
                 form)))))
    
    (def any (constantly true))
    
    (defrule calc-discount
      [member-type amount]
       "Guest"   (> 2000)  => 0.03
       "Silver"  (> 1000)  => 0.10
       "Silver"     (any)  => 0.05
       "Gold"     (> 500)  => 0.20
       "Gold"       (any)  => 0.15)
    
    (calc-discount "Silver" 1234) ;=> 0.10