clara-rulesを軽く触って一晩眠ってみるとclara-rulesで何ができるかもっと知りたくなっていたので自分で雑な問題をでっちあげてみた。
問題:
ある商品(Item)を所持金(MyMoney)で買えるかどうかを判定する。
- 商品の最終的な値段は以下のステップで計算される
- 割引き
- 課税
- Itemが割引対象
#{:fish :pants}
に含まれていれば10%値段を割引く - Itemにはcategory属性があり、それによって課税率が変わる
food 0.1
:cloth 0.2
:alcohol 0.3})```
- 割引き後、課税後の値段が所持金よりも少なければめでたく購入できる
##コード
```clj
(require '[clara.rules :refer [defrule fire-rules insert insert! mk-session]])
(def discounted-items #{:fish :pants})
(def tax-rates {:food 0.1 :cloth 0.2 :alcohol 0.3})
(defrecord MyMoney [amt])
(defrecord Tax [category rate])
(defrecord Item [id price category])
(defrecord DiscountedItem [discounted-price category])
(defrecord TaxedItem [final-price])
(defrule DiscountItem
[Item (= ?id id) (= ?category category) (= ?price price)]
=>
(println "Item is" ?id)
(println "Original Price is" ?price)
(println "Discounted item?" (some? (discounted-items ?id)))
(insert! (->DiscountedItem (* ?price 0.9) ?category)))
(defrule TaxItem
[DiscountedItem (= ?category category) (= ?discounted-price discounted-price)]
=>
(println "Tax rate is " (?category tax-rates))
(insert! (->TaxedItem (+ ?discounted-price (* ?discounted-price (?category tax-rates))))))
(defrule EnoughMoney
[MyMoney (= ?amt amt)]
[TaxedItem (= ?final-price final-price) (> ?amt final-price)]
=>
(println "Final price:" ?final-price)
(println "You have enough money!"))
(defrule NotEnoughMoney
[MyMoney (= ?amt amt)]
[TaxedItem (= ?final-price final-price) (not (> ?amt final-price))]
=>
(println "Final price:" ?final-price)
(println "You do not have enough money!"))
insert!とinsertの違い
- insert!はdefruleの中で使用し、暗黙に現在のセッションにファクトを追加する
- insertは明示的に対象とするセッションを引数として受けとる
defruleの左辺で変数を束縛することができる!
例えばDiscountItemルールの左辺の以下の例では、
- Itemのidフィールドを?id
- Itemのcategoryフィールドを?category
- Itemのpriceフィールドを?price
に束縛している。
[Item (= ?id id) (= ?category category) (= ?price price)]
- 変数は
?
で始まる - (= ?variable field-of-type)
- 束縛した変数は右辺、左辺のどちらからでも参照できる。(変数間に依存が無いものとして)
雑テスト
(-> (mk-session)
(insert (->MyMoney 199) (->Item :fish 200 :food))
(fire-rules))
;;Item is :fish
;;Original Price is 200
;;Discounted item? true
;;Discounted Price 180.0
;;Tax rate is 0.1
;;Final price: 198.0
;;You have enough money!
(-> (mk-session)
(insert (->MyMoney 197) (->Item :fish 200 :food))
(fire-rules))
;; Item is :fish
;; Original Price is 200
;; Discounted item? true
;; Discounted Price 180.0
;; Tax rate is 0.1
;; Final price: 198.0
;; You do not have enough money!
(-> (mk-session)
(insert (->MyMoney 215) (->Item :pants 200 :cloth))
(fire-rules))
;; Item is :pants
;; Original Price is 200
;; Discounted item? true
;; Discounted Price 180.0
;; Tax rate is 0.2
;; Final price: 216.0
;; You do not have enough money!
(-> (mk-session)
(insert (->MyMoney 217) (->Item :pants 200 :cloth))
(fire-rules))
;; Item is :pants
;; Original Price is 200
;; Discounted item? true
;; Discounted Price 180.0
;; Tax rate is 0.2
;; Final price: 216.0
;; You have enough money!
(-> (mk-session)
(insert (->MyMoney 521) (->Item :beer 400 :alcohol))
(fire-rules))
;; Item is :beer
;; Original Price is 400
;; Discounted item? false
;; Discounted Price 400
;; Tax rate is 0.3
;; Final price: 520.0
;; You have enough money!
(-> (mk-session)
(insert (->MyMoney 519) (->Item :beer 400 :alcohol))
(fire-rules))
;; Item is :beer
;; Original Price is 400
;; Discounted item? false
;; Discounted Price 400
;; Tax rate is 0.3
;; Final price: 520.0
;; You do not have enough money!
;;Forgot to insert MyMoney, what happens?
(-> (mk-session)
(insert (->Item :beer 400 :alcohol))
(fire-rules))
;; Item is :beer
;; Original Price is 400
;; Discounted item? false
;; Discounted Price 400
;; Tax rate is 0.3
感想
- ルールが必要とするファクトがinsertされていない場合、そのルールが発火しないのが興味深かった。
- コードで書いたEnoughMoneyとNotEnoughMoneyのルールが冗長なのでアンチパターンな気がしている
- Itemが複数あるときにどう処理していくのか見えないので次回はそのあたりを攻めてみたい
- プリント以外の副作用の可能性を探索してみたい
- retractがどう動作するのか調べたい