Closed telekid closed 3 years ago
Not sure how nextjournal works but i copied it locally and i always get this result:
card1633
Purchase failed
card1634
Purchase succeeded
Cards: [{:id card1633, :purchased false, :color :blue} {:id card1634, :purchased true, :color :red}]
Tokens in wallet: [{:token-type-id token-type1630, :color :red, :count 2}]
I think you changed it a bit after i copied it but the output doesn't seem to change on the website either. How can i make it rerun on there?
Sorry, I could have made that easier to reproduce. Here's some quick and dirty code to make it easier to test locally. If I have time later today, I'll see if I can make a more minimal reproduction.
Try throwing this in a buffer and calling (reproduce)
repeatedly:
(ns bug?)
(require '[odoyle.rules :as o])
(def rules
(o/ruleset
{::purchase-success
[:what
[::action ::attempt-purchase id]
[id :card/purchased? false]
[card-cost-id :card-cost/for-card id]
[card-cost-id :card-cost/color color]
[card-cost-id :card-cost/count needs-count]
[token-type-id :token/color color]
[token-type-id :token/count has-count]
:when
(<= needs-count (or has-count 0))
:then
(println "Purchase succeeded")
(-> o/*session*
(o/insert id :card/purchased? true)
(o/insert token-type-id :token/count (- has-count needs-count))
o/reset!)]
::purchase-fail
[:what
[::action ::attempt-purchase id]
[id :card/purchased? false]
:when
true
:then
(println "Purchase failed")]
::card
[:what
[id :card/purchased? purchased]
[id :card/color color]]
::token
[:what
[token-type-id :token/color color]
[token-type-id :token/count count]]}))
(defn initialize-wallet [session tokens]
(swap! session
(fn [session]
(let [tokens (mapcat (fn [[color count]]
(let [token-type-id (gensym "token-type")]
[[token-type-id :token/color color]
[token-type-id :token/count count]]))
tokens)
session (reduce o/insert session tokens)]
(o/fire-rules session)))))
(defn create-card [session card-id color costs]
(swap! session
(fn [session]
(let [card-cost-id (gensym "card-cost")
costs (mapcat (fn [[color count]]
[[card-cost-id :card-cost/for-card card-id]
[card-cost-id :card-cost/color color]
[card-cost-id :card-cost/count count]])
costs)
card [[card-id :card/color color]
[card-id :card/purchased? false]]
facts (concat costs card)
session (reduce o/insert session facts)]
(o/fire-rules session)))))
(defn attempt-purchase [session card-id]
(swap! session
(fn [session]
(-> session
(o/insert ::action ::attempt-purchase card-id)
(o/fire-rules)))))
(def i (atom 0))
(defn reproduce []
(def *session
(atom (reduce o/add-rule (o/->session) rules)))
(println "Run " (swap! i inc))
;; Put three red tokens in wallet
(initialize-wallet *session {:red 3})
;; Create a card and attempt to purchase it
(let [card-1-id (gensym "card")
card-2-id (gensym "card")]
(create-card *session card-1-id :blue {:red 4})
(create-card *session card-2-id :red {:red 1})
(attempt-purchase *session card-1-id)
(attempt-purchase *session card-2-id))
(println
"Cards:"
(o/query-all @*session ::card))
(println
"Tokens in wallet:"
(o/query-all @*session ::token))
(println))
(reproduce)
This is the result that I get from calling (reproduce)
three times in a row:
(reproduce)
Run 1
Purchase failed
Purchase succeeded
Cards: [{:id card30234, :purchased false, :color :blue} {:id card30235, :purchased true, :color :red}]
Tokens in wallet: [{:token-type-id token-type30233, :color :red, :count 2}]
nil
bug?> (reproduce)
Run 2
Purchase failed
Purchase succeeded
Cards: [{:id card30241, :purchased false, :color :blue} {:id card30242, :purchased true, :color :red}]
Tokens in wallet: [{:token-type-id token-type30240, :color :red, :count 2}]
nil
bug?> (reproduce)
Run 3
Purchase failed
Purchase failed
Purchase succeeded
Cards: [{:id card30248, :purchased false, :color :blue} {:id card30249, :purchased true, :color :red}]
Tokens in wallet: [{:token-type-id token-type30247, :color :red, :count 2}]
nil
Note that the third time, Purchase failed
is printed twice. That is surprising to me, but maybe it's normal? I'm new to this whole rules engine thing.
BTW, awesome work on this, and great talk. I'm excited to explore this further.
You found a bug :D I have pushed a fix: https://github.com/oakes/odoyle-rules/commit/28c0b3621b035eec8ebc686f39e8735c353c3e0e
The way your code should work is, it should always print "Purchase failed" twice. This is because your two facts always start out with :card/purchased? false
. The ::purchase-success
rule might change that, but by the time that :then
block runs, the ::purchase-fail
rule should already have been queued up to run. The bug was that :then
blocks could sometimes prevent rules from running even when they were already queued up.
I released 0.8.0 with the fix. Keep in mind that the order rules execute in is not guaranteed if they are not dependent on each other, so your example will print "Purchase succeeded" in different places each time you run it. But with this version it should always print "Purchase failed" twice. I'll close this but let me know anything still seems wrong.
Cool! Makes sense – thanks for looking into it!
I spent a bit of time fiddling about with odoyle this morning, and I was surprised to find that rule execution order seems to be nondeterministic. Is this known or intentional?
For an example of what I mean, see this nextjournal notebook. Notice that if you run it multiple times, the order of "purchase failed" / "purchase succeeded" changes. Sometimes "purchase failed" doesn't execute at all. Why is that?