Saturday 10 August 2013

Duck-State management - or how I am unable to deal with state

People are afraid of typeless programming environments, and I might have become an example of why. The merpg.State - namespace of the MERPG-engine, written in Clojure (and available here, although the last commit pushed out of this Mac is at least a month old...), has become a vast, typeless trap for the unwary adventurer, if I may quote this excellent expression. How has this happened? How am I going to advance from here, now that I've accepted the situation? Well, let's find out!

I know you all know this, but let me revise the Clojure state-system here, since it's a prerequisite to understanding any of my blabbering in this text. The baseline is this: there are no typed vars, only (JVM-)typed values. Functions are allowed to take any kind of params, and return whatever they want. It seems rather identical to the type system of the Ruby, for example, which is an absolutely intriguing language, one which I hope to be able to make something with in the future. Anyway, this type(less)-system you're accustomed to in other Lisps or Ruby has one major difference compared to the just-mentioned languages: there are no destructive operations. You can't re-set the the variable (without resorting to functional hacks), and if you have a vector [2 3 4 5], you can be certain no one is going to cons stuff into it, since, you know, it is impossible to do so. (assoc that-vector 0 10) returns previously mentioned vector with the first element set to 10 instead of 2, and leaves the original untouched. In some hypothetical language following Ruby's syntax and Clojure's naming semantics, one would use .add!(elem) - method to add stuff to a list, exclamation marking the fact that this method probably returns void or the object upon which it is called, and instead edits the list in place. However, there are no exclamation-point-functions in Clojure (well, there are, but you don't need to know it :), so there are no in-place-edits on data structures, so data-structures are really immutable, and as a side effect we have a thread-safe system, and the coder has to write some little elegance to work around the limits immutable values present.

Remember when I told you there's no way to assign new values to variables? Well, I lied! There are a couple of wrappers that enable state management in a functional, thread-safe way. There are refs, which have to be used inside transaction (and in case system blows up inside the transaction, every ref already set inside that transaction is returned to the original value), with functions whose use is explained lousily in every doc I've been able to find. Then there are agents, which I have also not used in my codes, so I may be talking rubbish here, but if I recall, they present same interface for setting their value as atoms, and their new values are calculated in new threads. And then, my favourite, atoms: swapping their value is done with (swap!) - function. It takes the atom-to-be-set and a function taking the old value as a param and returning the new value as the parameters. Not counting that this is an atomic operation, rest of the system won't see "intermediate" values, the elegance of this systems stems from the fact you can... well... calculate the new value with any function. Newbies could increment an integer-atom with (swap! atom #(+ 1 %)), when in fact the built-in (inc) is basically the same as #(+ 1 %).

(At least) refs and atoms have a couple of cool features: validators, which are what java's type system wants to be once it has grown up. If you swap! something irresistibly stupid into an atom that has a validator designed to catch that kind of values, the validator throws an exception. Another cool feature is: watches, which can be used to raise warnings in case where validators raise errors, but there are other use cases. Watches are really what WPF's INotifyPropertyChanged-databindingsystem wants to be once it grows up. Once someone swap!s new values into the state, watches get called, and inside these watches we rebuild the GUI (a hairy area we're not going to talk about, although Seesaw makes GUI-stuff rather bearable compared to Swing, Winforms and WPF).

Then there's the really hairy stuff known as the java-interop, but we don't want to go there, do we?

Hopefully I have built up enough base on which to tell you about my lousy state-management

The problems

Because of the Clojure's duck-typing, there's no need to bother what gets sent to the map-editing functions. You need to have a vague idea of what is a sequence, what is a sequence of sequences and what is a basic tile. It is the same syntax for every case. But if we remember we're analysing the State - namespace, we know there has to be some atoms, and atoms require special syntax. Special syntax is baaad :) Well, no, since having to (deref) stuff would be worse. Thus user-definable reader macros would be nice, but that's stuff for another rant.

Although special syntax of atoms isn't necessarily bad, leaking it outside State-ns is. So first tenet of the refactored State-ns is: no leaked atoms. If the coder requires access to atoms, tough luck: use either setters for watches or getters, which return the derefed value.

And of what will these atoms consist of? Well, tiles, which are easy. They are just simple maps: {:x :y :tileset :rotation}. Then there are the layer-generators, from which you may take layers by (take Width (layerrow W H Name))ing. Thusly layers look like this: {:title name :transparency transparency :layer [[tile tile...][tile tile...][tile tile...]...]}. In case one has to change tiles in this structure, which would seem hard considering this map is immutable, one could use code like this:

 (let [layer (:layer %)]
 (assoc layer x
        (assoc (nth layer x) y new-tile)))
      
although one should be wary of layer being a lazy-seq and thus M-% layer RET (vec layer) RET. This code first destructures the layer for us, then updates xth row to be thing, that's returned after updating yth tile in the xth row to new tile. The syntax may seem weird to a C-world dweller, but how would one define such a construct in C? Well, layer[x][y] = newtile; is easier to write, but the cases where C-syntax is better than Lispy lack-of-such are rare, and even this lacks in A) the thread-safety B) Watch-department

Layer defines apis for setting title (which is going to be awesome if I'm forbidding the leaking of the atoms \o/. But as Paul Graham said, in dynamic world one can write code as if sketching with a pencil, instead of sculpting granite), transparency, tiles (according to the previous code snippet). We also need getters for all the previous stuff to prevent leaking the atoms, but they sure-as-hell are not going to be named javaesquely getX. I think we need add-watches for the :layer - atoms too.

The root-level atoms are the maps. Maps are basic, ordered vectors (pun very-much intended :) of layers. Structure looks something like this: [{:title name :transparency transparency :layer [layer]} {:title name :transparency transparency :layer [layer]}]. This structure defines api for adding new layer, adding watch for new layers, getting layer per index and moving layers up or down.

Then there is the great root: map-set. Although the name suggests otherwise, I'll probably implement this as a vector of maps. One can add- and remove maps, and although this is logically unordered set, I'll write ordering semantics for the editor. The problem in this implementation: how am I going to represent the relationships of maps (also known as "displacement vectors" in the old code, meaning the coordinates ({:map :x :y}) where players will be transferred to other map ({:index-of-another-map :x :y}). Well... maybe the mapset looks like this: {:displacement-coordinates [[{:map :x :y}{:map :x :y}][another-two-tuple]] :maps [map another-map third-map etc.]}.

And as already mentioned twice (or thrice? I'll repeat it once more, just to remind myself of it): Atoms are not to be leaked outside the State-ns! A (deref)-call, @-char and (add-watch)-call all equal pure fail. Not a procedural fail, not an OO-fail, but a pure, functional fail.

And with these words, I hereby conclude this text, publish it without any cleanup, and C-x b myself back to the State.clj

No comments:

Post a Comment