This post could also be entitled How I Learned to Love Immutability, and You Won’t Believe What Happened Next!
A few weeks ago I released a library called Brute, which is an Entity Component System Library for Clojure. This was the first Clojure library I have released, and I wanted it to be as easy to use as possible. Therefore, falling back on my imperative roots, I decided to maintain the internal state of the library inside itself, so that it was nicely hidden from the outside world.
That should have been my first red flag. But I missed it.
The whole time I was writing the library, I kept having thought of “what happens if two threads hit this api at the same time” and worrying about concurrency and synchronisation.
That should have been the second red flag. But I missed it too.
So I released the library, and all was well and good with the world, until I got this fantastic piece of feedback shortly thereafter. To quote the salient parts:
In reading this library, one thing stuck out to me like a sore thumb: every single facet of your CES is stored inside a set of globally shared atoms.
After a bit of back and forth, there was a resounding noise as the flat of my palm came crashing into the front of my face.
Two items on the list of core foundations of Clojure are:
- Pure Functions
Rather than adhere to them as much as was pragmatically possible, I flew in completely the other direction. Brute’s functions had side effects, changing the internal state that was stored in it’s namespace, rather than just simply keeping my functions pure and passing around simple, immutable data collections. This made it icky, very constrained in its applications, and also far harder to test. All very bad things.
So I’ve rewritten Brute to be pure, not to maintain state internally, and simply pass around an immutable data structure, and this has made it far, far better than the original version.
Looking at the API, it’s not a huge departure from the original, but from a functional programming perspective, it’s like night and day. Suddenly all my concerns about concurrency and data synchronisation with each function call are gone – which is one of the whole points of using Clojure in the first place.
To start with Brute, now you need to create its basic data structure for stories Entities and Components. Since Brute no longer stores data internally, it is up to the application to store the data structure state, and also choose when the appropriate time is to mutate that state. This makes things far simpler than the previous implementation. It is expected that most of the time, the system data structure will be stored in a single
reset! on each game loop.
(def system (atom (brute.entity/create-system))
From here, (almost) every Brute function takes the
system as it’s first argument, and returns a new copy of the immutable data structure with the changes requested. For example, here is a function that creates a Ball:
"Creates a ball entity"
(let [ball (e/create-entity)
center-x (-> (graphics! :get-width) (/ 2) (m/round))
center-y (-> (graphics! :get-height) (/ 2) (m/round))
ball-center-x (- center-x (/ ball-size 2))
ball-center-y (- center-y (/ ball-size 2))
(e/add-component ball (c/->Ball))
(e/add-component ball (c/->Rectangle (rectangle ball-center-x ball-center-y ball-size ball-size) (color :white)))
(e/add-component ball (c/->Velocity (vector-2 0 300 :set-angle angle))))))
The differences here from before are:
create-entitynow just returns a UUID. It doesn’t change any state like it did before.
- You can see that
systemis threaded through each call to
add-component. These each return a new copy of the immutable data structure, rather than changing encapsulated state.
This means that state does not change under your feet as you are developing (which it would have in the previous implementation). This makes developing your application a whole lot simpler and easier to manage and develop.
There are also some extra benefits by rewriting this library as well:
- How the entity data structure is persisted is up to you and the library you are using, which gives you complete control over when state mutation occurs – if it occurs at all. This makes concurrent processes much simpler to develop.
- You get direct access to the ES data structure, in case you want to do something with it that isn’t exposed in the current API.
- You can easily have multiple ES systems within a single game, e.g. for sub-games.
- Saving a game becomes simple: Just serialise the ES data structure and store. Deserialise to load.
- Basically all the good stuff having immutable data structures and pure functions should give you.
Hopefully this also helps shed some light on why immutability and purity of functions are deemed good things, as well as why Clojure is also such a great language to develop with.