- Clojure is idiomatically functional programming
- Most state is immutable
I took the idea from the slide "Everything is a Map" (From 11 minutes, 6 seconds to > 29 minutes in). Some things he says are:
- Whenever you see a function that takes 2-3 arguments, you can make a case for turning it into a map and just passing a map in. There are a lot of advantages to that:
- You don't have to worry about argument order
- You don't have to worry about any additional information. If there are extra keys, that's not really our concern. They just flow through, they don't interfere.
- You don't have to define a schema
- As opposed to passing in an Object there's no data hiding. But, he makes the case that data hiding can cause problems and is overrated:
- Ease of implementation
- As soon as you communicate over the network or across processes, you have to have both sides agree on the data representation anyway. That's extra work you can skip if you just work on data.
Most relevant to my question. This is 29 minutes in: "Make your functions composable". Here's the code sample he uses to explain the concept:
;; Bad (defn complex-process  (let [a (get-component @global-state) b (subprocess-one a) c (subprocess-two a b) d (subprocess-three a b c)] (reset! global-state d))) ;; Good (defn complex-process [state] (-> state subprocess-one subprocess-two subprocess-three))
I understand the majority of programmers aren't familiar with Clojure, so I'll rewrite this in imperative style:
;; Good def complex-process(State state) state = subprocess-one(state) state = subprocess-two(state) state = subprocess-three(state) return state
Here are the advantages:
- Easy to test
- Easy to look at those functions in isolation
- Easy to comment out one line of this and see what the outcome is by removing a single step
- Each subprocess could add more information on to the state. If subprocess one needs to communicate something to subprocess three, it's as simple as adding a key/value.
- No boilerplate to extract the data you need out of the state just so that you can save it back in. Just pass in the whole state and let the subprocess assign what it needs.
Now, back to my situation: I took this lesson and applied it to my game. That is, almost all of my high level functions take and return a
gameState object. This object contains all the data of the game. EG: A list of badGuys, a list of menus, the loot on the ground, etc. Here's an example of my update function:
update(gameState) ... gameState = handleUnitCollision(gameState) ... gameState = handleLoot(gameState) ...
I'm worried that one day I'll wake up and realize this whole design is a sham and I've really just been implementing the Big Ball Of Mud anti-pattern.
Honestly, I've been working on this code for months and it's been great. I feel like I'm getting all the advantages he's claimed. My code is super easy for me to reason about. But I'm a one man team so I have the curse of knowledge.