Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to implement a decision matrix/table in Clojure

Tags:

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?

like image 498
Alfred Xiao Avatar asked Dec 05 '22 07:12

Alfred Xiao


2 Answers

Use core.match! It's a Clojure library for pattern matching.

Your example would turn out a little something like this....

(let [member-type "Gold"
      amount 600]
  (match [member-type amount]
         ["Guest" (_ :guard #(> % 2000))] 0.03
         ["Silver" (_ :guard #(> % 1000))] 0.1
         ["Silver" _] 0.05
         ["Gold" (_ :guard #(> % 500))] 0.2
         ["Gold" _] 0.15
      :else 0))

 ; => 0.2
like image 73
Daniel Neal Avatar answered Jan 12 '23 11:01

Daniel Neal


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
like image 31
A. Webb Avatar answered Jan 12 '23 11:01

A. Webb