This is second part in series about implementing finite-state machine DSL in Hy. The first part is here. Lets start by comparing where we left of in the previous post and what I ended up with in the end (or at least at the time of writing this entry).
The original idea for the code was as following:
=> (statemachine TwistedAccumulator ... :initial-state addition ... :interface [message] ... (addition (on-activate (state bonus 0)) ... (active (when (odd? message) ... (state bonus message)) ... (+ message (state bonus))) ... (transitions [(= message 0) subtraction])) ... (subtraction (active (when (odd? message) ... (state bonus message)) ... (- message (state bonus))) ... (transitions [(= message 0) addition]])))
And the end result is shown below:
=> (defstatemachine TwistedAccumulator [message] ... "twisted accumulator demonstrates shared state" ... ... "add bonus to message" ... (addition initial-state ... (on-activate (state bonus 0)) ... (active (when (odd? message) ... (state bonus message)) ... (+ message (state bonus))) ... "send 0 to transition to subtraction" ... (transitions [(= message 0) subtraction])) ... ... "substract bonus from message" ... (subtraction (active (when (odd? message) ... (state bonus message)) ... (- message (state bonus))) ... "send 0 to transition to addition" ... (transitions [(= message 0) addition])))
Basic structure is almost the same and one can easily tell that they do the same thing. However, there are some changes here and there. Some of them were done because they made the language easier to use in my opinion, some were done purely for technical reasons.
First change was to replace statemachine with defstatemachine. This made indentation more palatable with my current settings and it has nice symmetry with defn used to define functions.
Second change is similar. Earlier example used separate keyword to define interface of finite-state machine. The later example just has all accepted parameters as a list after type name, just like defn does. This makes it easier to spot the interface definition on a glance, as reader doesn’t have to hunt it down in the same space with state definitions.
Originally I envisioned initial state being specified as a keyword symbol combination. However, it was much easier to code system where initial state is specified within one of the state specifications. I’m not entirely happy with this, since now initial state isn’t clear on a glance and there needs to be code that checks that only one of the states have been specified as initial. This part of the language might still change, depending on how I feel after using it for a bit.
At this point, only forms directly inside of the defstatemachine were used to specify type, interface and states of the finite-state machine. I added possibility of having strings there as comments. That combined with a little bit whitespace here and there makes much readable code in my opinion. If there had been :initial-state and :interface keywords, filtering strings out would have been much more trickier.
Definition of states didn’t change much between the initial drafting and first version of the DSL. Only initial-state and comment strings were added.
At this point the DSL is fully functional and tested with some sample finite-state machines. I haven’t yet used it in game, but expect to do that relatively soon. Probably I’m going to try it out on AI routines and start by rewriting some old code to get the feel of the language.
Here’s an example test I wrote while programming the macro. It uses the TwistedAccumulator shown earlier in this post:
(fact "fsm - twisted - state can be modified" (with-background twister [fsm] (assert-that (fsm 2) (is- (equal-to 2))) (assert-that (fsm 1) (is- (equal-to 2))) (assert-that (fsm 2) (is- (equal-to 3))) (assert-that (fsm 3) (is- (equal-to 6)))))
This partly highlights difficulty of testing macros (it’s also pretty hard test to follow, you put 2 in and get 2 out. You put 1 in and get 2 out and so on..), as in this case I’m testing the finite-state machine created by the macro. Actually testing the macro would require analysing resulting code and reasoning if it behaves in the expected way. And that’s way beyond what I can do.