If my coinflip statechart has two states: heads and tails, is there a more concise/correct way to randomly transition between the states on a :flip event?
(def coin-flips
(let [heads :id/heads
tails :id/tails
pick-rand #(rand-nth [heads tails])]
(statechart {}
(state {:id heads}
(transition {:event :flip :target tails :cond (fn [_ _] (= tails (pick-rand)))})
(transition {:event :flip :target heads :cond (fn [_ _] (= heads (pick-rand)))}))
(state {:id tails}
(transition {:event :flip :target heads :cond (fn [_ _] (= heads (pick-rand)))})
(transition {:event :flip :target tails :cond (fn [_ _] (= tails (pick-rand)))})))))
I know about the choice macro, but I'm more-so curious if I can set the :target dynamically and avoid having to define two possible conditional transitions from each state.So, the viz stuff is working relatively well. It could really use some dolling up by any interested party, but it is working for me. It will not be hard at all to make it work for CLJ via electron, or a pathom resolver through Fulcro itself. It’s all the same stuff, though it renders in js. That said, the code uses the ELK layout library, so writing a CLJ only version of the viz that uses the JVM should also be really straightforward…just mimic the exact same calls to the Java version of ELK.
looks like I have a bug in history node rendering… 😞 So it doesn’t always do the layout
You probably want to only call the pick-rand function once.
(def coin-flips [:coin/heads :coin/tails])
(defn pick-rand [& _] (rand-nth coin-flips))
(statechart {:id :coin-flip-statechart}
(state {:id :coin}
(state {:id :coin/flip}
(on-entry {:id :entry/coin-flip}
(assign {:id :assign/coin-flip->coin-result
:location [:coin/result]
:expr pick-rand}))
(transition {:id :transition/coin-flip->coin-heads
:target :coin/heads
:cond (fn [_ {:coin/keys [result] :as data}]
(= result :coin/heads))})
(transition {:id :transition/coin-flip->coin-tails
:target :coin/tails
:cond (fn [_ {:coin/keys [result] :as data}]
(= result :coin/tails))}))
(state {:id :coin/heads})
(state {:id :coin/tails})
(transition {:id :transition/coin->coin-flip
:event :flip
:target :coin/flip})))
I did not test this I don't have a boilerplate handy to setup a simple statechart but this reduces your random pick to once per event, and then the transition depends on the result. It might need to be tweaked.Why not just have two top-level transitions. The first uses a predicate that returns a rand boolean. If true it goes to heads. Then a transition on the same event after that with no cond that always goes tails
(statechart {:id :coin-flip-statechart}
(state {:id :coin}
(transition {:target :coin/heads
:event :event/flip
:cond (fn [& _] (rand-nth [false true)})
(transition {:target :coin/tails
:event :event/flip})
(state {:id :coin/heads})
(state {:id :coin/tails})))
multiple transitions on the same event have precedence in doc order.
@tony.kay that's nice and concise. Is there a good reason why :target cannot be a fn or have something like :targetexpr to achieve the same effect? Is it because this would be a divergence from the reference work?
it would diverge from reference enough that it could affect compatibility. The reference work covers the cases well, and having a “dynamically changing” edge in a chart breaks a lot of things (e.g. visualization, which is quite important and one of the core benefits)
@michael819 thanks for catching my bug and for the advice. I like how you've namespaced many of the keyword args so it's more clear what they belong to. I think I'll adopt that habit as well. I assume there isn't a common convention used for naming these things so just using something consistently is probably the best bet.
I also tried to make use of the data model / assign as you do in your example. It would definitely be handy in a bigger statechart, but in this case I'm just trying to learn the ropes and take a minimalist approach.
Thanks again for your snippet, it was helpful.
@tony.kay yeah, that makes perfect sense. Is that viz stuff ready to try at all at this point? I haven't got around to upgrading Inspect yet and my current SC experiments are just cljc and no Fulcro.
@alex.sheluchin The naming convention I came up with because when you are looking at the debug logging output all the randomly generated ids made it much harder to understand what was happening in the state. I have adopted it for my statecharts projects and it makes the debug log much easier to follow.
I covered some of the lessons learned from this thread in a blog post: https://fnguy.com/fulcro_statecharts.html Thanks @tony.kay and @michael819 for your input.
This is a very well written article and I think you nailed it in regards to illustrating how to reason about simplifying states and showing a couple easy to follow examples. Excellent job! I look forward to reading more in your series.