3
votes

I have a game state represented as a map and some logic that updates that state on every game 'tic'. But I can't figure out how to structure the update function in any sane way.

What is the idiomatic pattern for structuring functions like this?

Here is some pseudo code for what I want to do:

(defn tic [g] "Return an updated game"
  g1 = (update-in g [:day] inc)
  g2 = (if (some-cond) (some-update-func g1) g1)
  g3 = (update-in g2 [:fu] fu-update)
  ... many more ...
  g-last)

I don't really care about the intermediate states, but using the -> macro doesn't work (since there are some conditionals).

A hack that works is using a local atom that is reset! for every 'line' in the update function. But that can't be how it's supposed to be done?!

2

2 Answers

6
votes

I would suggest to extract each of the steps in a nicely named function, so that you can use ->. Pseudo code:

(defn tic [g]
    (-> g
        inc-day
        random-weather
        grow-trees
        ...))

For any conditional logic, you can just do something similar to what you do in your g2 step.

Perhaps you will find synthread lib useful. I found this video very instructive.

Look also at cond-> to see how could you mix -> with some cond. For example your cond could look like:

(cond-> g
        true (update-in [:day] inc)
        (some-cond) some-update-fund
        true (update-in [:fu] fu-update))
3
votes

You can still use -> if for the conditional steps, you wrap the operation in an anonymous function.

(-> g0 
    ...
    (#(if (some-cond) (u %) %))
    ...)

If you are concerned about efficiency (you mentioned this is a game) I would recommend you use cond-> or maybe create your own macro. cond-> requires repeating true for expressions that are always threaded, which may be tedious, depending on the number of items in your thread.

Here is a macro that can be used in conjuction with -> that avoids excessive creation of anonymous functions and avoids the repetitiveness of cond->:

(defmacro maybe [val sym cond expr]
  `(let [~sym ~val] (if ~cond ~expr ~sym)))

It can be used like this:

(-> g0
    ...
    (maybe gn (some-cond gn) (updater gn))
    ...)

If you don't need to use the partially-processed value gn in the condition expression, you can just use cond-> instead of maybe:

(-> g0
    ...
    (cond-> (some-cond g0) updater)
    ...)

Another example:

(-> 10 (maybe gn (= gn 10) (* gn 100)))

Which evaluates to 1000