Should I better use a state monad (and how)?

164 views
Skip to first unread message

Nicolas Buduroi

unread,
Apr 15, 2012, 11:25:21 PM4/15/12
to clo...@googlegroups.com
I'm working on a turn-based game and I'm looking for a good way to manage states. In the game each turn is composed of multiple phases. I started by using atoms for the phases field (this is a sequence of functions) in a record and realized that it wouldn't be ideal to keep track of states in the case where I'd need to keep a snapshot of every phases. Here's the original code I had:

(defrecord Game [phases]
(next-phase [this]
(stop-timer)
   (swap! phases #(conj (vec (rest %)) (first %)))
   (log :info "change phase to %s" (key (first @phases)))
(start-phase this))

I then started to think that this would be a good opportunity to use a state monad. I've tried to reimplement the above code using the algo.monads library but the result was less than satisfactory (probably due to my own shortcoming), here's the monadic version:

(defrecord Game [phases]
  (next-phase [this]
    (->
  ((domonad state-m
     [_ (fn [s] (stop-timer) [s s])
      _ (update-state
         (fn [s]
           (update-in s [:phases]
                      #(conj (vec (rest %)) (first %)))))
      _ (fn [s]
          (log :info "change phase to %s" (key (first (:phases s)))) [s s])]
     nil)
   state)
  second
  start-phase))

As my code probably doesn't need the full power of the state monad, I tried to write a lighter-weight version using the following macro:

(defmacro >> [& state-and-forms]
(reduce #(list (if ('#{fn fn*} (first %2))
%2
`(fn [s#] ~%2 s#)) %)
state-and-forms))

Which let me write:


  (next-phase [state]
    (>> state
     (stop-timer)
     (fn [s] (update-in s [:phases] #(conj (vec (rest %)) (first %))))
     #(do (log :info "change phase to %s" (key (first (:phases %)))) %)
     #(start-phase %)))

With some more helper macro this version looks promising. In the end I wonder if there's some Clojure feature I'm overlooking or if I should rethink the whole solution? Is there a better way to accomplish this?


kovas boguta

unread,
Apr 16, 2012, 12:08:30 AM4/16/12
to clo...@googlegroups.com
You can try using the in-memory version of Datomic.

Besides keeping track of the state at every point, it can help with
the reasoning about what should happen next for each state change.

> --
> You received this message because you are subscribed to the Google
> Groups "Clojure" group.
> To post to this group, send email to clo...@googlegroups.com
> Note that posts from new members are moderated - please be patient with your
> first post.
> To unsubscribe from this group, send email to
> clojure+u...@googlegroups.com
> For more options, visit this group at
> http://groups.google.com/group/clojure?hl=en

Nicolas Buduroi

unread,
Apr 16, 2012, 12:34:21 AM4/16/12
to clo...@googlegroups.com
On Monday, April 16, 2012 12:08:30 AM UTC-4, kovasb wrote:
You can try using the in-memory version of Datomic.

Besides keeping track of the state at every point, it can help with
the reasoning about what should happen next for each state change.


Hum, I hadn't though about using a service like Datomic at all, but I'm not sure it fill the bill has it looks like a very heavyweight solution. I'm not even sure where to start to understand what Datomic is!

Nicolas Buduroi

unread,
Apr 16, 2012, 12:37:09 AM4/16/12
to clo...@googlegroups.com
I couldn't resist writing some more macros!

(defmacro << [k-or-ks f]
  `(fn [state#]
     (update-in state#
                ~(if (vector? k-or-ks)
                   k-or-ks
                   (vector k-or-ks))
                ~f)))

(defn >>* [state form]
  (condp = (first form)
    '>>> `(fn [~state] ~@(rest form) ~state)
    '<<< `(fn [~state] ~@(rest form))
    form))

(defmacro >> [state & forms]
  (reduce #(list (if (#{'<< 'fn 'fn*
                        'clojure.core/fn
                        'clojure.core/fn*} (first %2))

                   %2
                   `(fn [s#] ~%2 s#)) %)
          (conj (map (partial >>* state) forms)
                state)))

With those the resulting code looks pretty clean and remain purely functional:


  (next-phase [state]
    (>> state
      (stop-timer)
      (<< :phases #(conj (vec (rest %)) (first %)))
      (>>> (log :info "change phase to %s" (-> state :phases first key)))
      (<<< (start-phase state))))

Now, I'd need to find better names!

kovas boguta

unread,
Apr 16, 2012, 12:49:46 AM4/16/12
to clo...@googlegroups.com
Its just a library with some functions; if you can understand macros
and the STM I'm confident you can understand it too.

Alan Malloy

unread,
Apr 16, 2012, 3:36:40 AM4/16/12
to Clojure
#(conj (vec (rest %)) (first %)) is a really awful way to implement a
queue. Just use clojure.lang.PersistentQueue, which works with the
conj/peek/pop functions in clojure.core.
Reply all
Reply to author
Forward
0 new messages