Saturday 18 February 2012

Generating functions

Macros in LISPs are fascinating, and from what I gather, they are fairly well implemented in Clojure. As I'm sure I've mentioned before, I'm writing a game in it, and recently I wrote a piece of code I find very interesting.

In that game, I have units, and those units have attributes like health, attack power and so on. I wanted to write a bunch of functions to get, set, modify and reset those values. Thats four functions times seven attributes... 28 functions. Naturally, it would be crazy to spell out ALL of those accessors by hand, especially since they are almost identical to each other in many cases.

What I did is made of three parts, basically.

First, I defined a set of attributes, which will be used later.

Second, I wrote generic accessors, accepting a keyword naming the attribute to work on as first argument, and whatever else they needed as the remaining arguments. I called them set-, get-, change- and reset-.

Last, and this is the most fun part, I wrote a little tidbit of top-level code:

(doseq [stat stats, f ["get-", "set-", "change-", "reset-"]]
  (eval `(def ~(symbol (str f (->> stat str rest (apply str))))
           (partial ~(symbol f) ~stat))))

While I didn't use defmacro explicitly, I use the macro mechanics here. I just wrote it inline. What happens here isn't very difficult to understand once you grasp the basics of macros, but I will step through it nonetheless.

The first line can basically translated as "for every possible combination of stats and those string, do the following".

eval executes the code it is given. Executing LISP code literally means just running it through eval. Also, it is a special form. But nevermind that right now.

The backtick ` is a very pleasant thing introduced in Clojure to combat common macro problems which aren't important right now, but generally it does the same as an apostrophe ', it turns the expression it precedes into a literal (when writing macros in Clojure, use backticks, never apostrophes - ALWAYS). So while (+ 2 5) evals to 7, '(+ 2 5) evals to... well, '(+ 2 5). The other one, the wave ~ (or however it is called), kind of cancels the quoting. As an example, '(+ ~(+ 1 1) 5) evaluates to '(+ 2 5).

Now it becomes fairly obvious what is happening here. We plug the attribute and function names into a generic def.

As an example, for :attack and "get-"...

`(def ~(symbol (str "get-" (->> :attack str rest (apply str))))
   (partial ~(symbol "get-") :attack))))

Which then turns into...

`(def get-attack
   (partial get- :attack))

And that is given to eval to finally execute it.

The abominable first line happens due to my choice of using keywords - running a keyword through str returns it together WITH the colon, and I remove it with the str rest (apply str) part. The second line is fairly straightforward. This is why it was important that the name of the attribute be the first argument.

Anyways, now, whenever the file is executed (be it by loading or by... well, running it), 28 functions are generated. I can easily add more attribute accessors by adding entries to the list of attributes, I can easily change the implementations of the functions themselves without having to wade through tons of code and correcting them in several places at once (which is a really error-prone procedure). Also, should I never need to, I can add new types of accessors with a minimum of hassle. And finally - I had fun writing accessors. When was the last time that ever happened to any of you?

I'm am fairly confident I am entitled to say I did an awesome job there.
Now I should go play Guitar Hero "Fury Of The Storm" on hard before my self-esteem gets too high.

PS: I slightly modifed the generic def later. The first line turned into
`(def (->> stat str rest (apply str) (str f) symbol)
It doesn't change anything, but looks more consistent, in my opinion.

PPS: The base functions (get-, set-...) are private. Just saying.

Saturday 4 February 2012

Destructuring: Addendum

Today, while continuing the work on my game, I remembered one thing about destructuring, and found out another.

Now, the one very important thing I remembered:

(let [[a b c :as d] [1 2 3]] (println a b c d))

results in

1 2 3 [1 2 3]

You can use :as in both vector and map destructuring.

The second thing... I tried it on a hunch, really.
In a very abstracted sense, you can treat vectors as maps with ordered integer keys.
So, a function I wrote got input in the form of a seq (the general sequence datatype of Clojure) of vectors. The vectors contained three elements each, but the only one I needed was the second one (index 1, in other words).

So I tried this...

(let [{thing 1} hypothetical-3-element-vector]
  (println thing))

Now, assuming hypothetical-3-element-vector is bound to, say [1 "lol" 2], the result will be...

lol

So not only can YOU hypothetically treat vectors that way, Clojure does so. In a way.
It probably isn't very useful that often, but still a neat thing to know.

[Edit; Afterthoughts resulting from  a walk with my dogs:]
Now that I think of it, this implies that destructuring, under the hood, uses get (or at least a similar mechanic). get handles both vectors and maps gracefully (which neatly extends to get-in... on which I rely a lot.)

So... I consulted The Joy of Clojure (get this book.). Apparently, there is so much more awesomeness hidden in destructuring. But won't discuss it here/now. Sadly it doesn't reveal anything about the inner workings, though. Will need to search later.