Recently, I've been doing something that involved some statistics (...I've been trying various formulae for calculating combat damage. Eh.), as well as some randomness (dice rolls, I admit it. -.-").
More clearly: The formulae include randomness as part of their calculations. I, when making tables and lists of results, do not want any randomness. Instead of modifying the function definition, or adding some hacky additional parameter, the entire thing can be solved in a rather elegant manner via dynamic bindings.
Dynamic bindings are a mechanism that allow you to... aw, shucks. I can't express it in one sentence, at least not clearly. Consider this example instead:
(def foo 2)
(defn bar []
(let [foo 4]
(baz)))
(defn baz []
(println foo))
Rather obviously, the output is 2. After all, the redefinition of "foo" is contained to the lexical context of the "let" within bar. Now consider this:
(def ^:dynamic *foo* 2)
(defn bar []
(binding [*foo* 4]
(baz)))
(defn baz []
(println *foo*))
This will print 4. At first I thought this kinda goes against the idea of pureness, otherwise so prevalent in Clojure. But as with everything, the only important thing is how you use it. As I've said, there are things that can be solved in a more elegant way thanks to this tool.
Consider something as mundane as I/O redirection. Clojure makes, of course, use of streams, and all I/O operations use, per default, the standard input and output streams. Now, sometimes we want to use different streams... a naive way would be then to simply add optional arguments to the I/O functions and be done with it. Except... it's ugly. Really. Wouldn't it be more awesome if we could just say "Those operations? Do them on this stream instead, kthx."? With dynamic bindings, we can do exactly that. clojure.core defines the dynamically bindable var *out*, the value of which is the currently used stream. So, we might do this...
(binding [*out* (some-file-or-whatever)]
(println ":D:D:D")
In the scope of this binding, all I/O operations that are aware of *out* will act on the new stream instead. Pretty cool, eh?
Anyways, dice, randomness. You might already see what I did there. And either way, I don't think it requires much more explanation...
(def ^:dynamic *dice-mode* :random)
(defn roll-dice [[amount sides modifier]]
(case *dice-mode*
:random (reduce + (or modifier 0) (for [i (range amount)] (inc (rand-int sides))))
:average (int (Math/round (+ (* amount (/ sides 2)) modifier)))
:zero 0))
Yes. In the part where I do the statistics, I can now either take the average of dice rolls, or simply completely omit them, via a simple binding.
You might be wondering about why I'm using asterisks. It's not per se required to use them. It's just, you are supposed to name dynamic variables this way. Clojure will give you a stern talking to if you use a dynamically bindable var and do NOT assign it a name surrounded with asterisks (or when you use such a name with regular vars). I guess the reason is readability? I'm not sure though. I don't mind either way, so I'm not likely to look it up. You can, though.
N.
No comments:
Post a Comment