Parts 1 through 4 of this series can be found here, here, here and here.
At long last, here is part 5 of my series translating the code examples from w(p)gtr into Clojure! I meant to post this like, 4 months ago, but I’ve been super busy, mostly with music stuff (see my last post for the most recent cool thing I’ve done), and I didn’t get a chance to test this code to make sure it works until just a few days ago. If anyone was really looking forward to this installment, sorry!
Here’s where things really get interesting. _why decides to throw some metaprogramming at us, showcasing an area where Ruby truly shines. Dwemthy’s Array is without a doubt the coolest part of the entire guide. The fact that you can mold and shape the syntax of Ruby to make your own custom DSLs is flat-out inspiring. As it turns out, this is another area in which Clojure (actually, Lisp in general) excels, with its macro system. If I wanted to write an idiomatic implementation of Dwemthy’s Array in Clojure, I would probably actually avoid using metaprogramming and instead use a more functional style and ordinary maps instead of records. But that wouldn’t address the burning question I had going into this translation project, which is essentially, “Can Clojure do everything Ruby can?” So, I took this as an opportunity to compare metaprogramming and classes in Ruby to metaprogramming and records in Clojure.
Also of note, the beginning of this chapter reminded me of how ugly Clojure’s (really Java’s) lower-level methods for dealing with webpage retrieval are, when compared to Ruby’s File
class and open-uri
library. This is something I noticed back in part 2 when dealing specifically with file I/O. Clojure’s spit
and slurp
methods are nice, but in order to read a file (or website content) in line by line, we still have to resort to Java inter-op and deal with explicit reader and writer objects. You’d think there’d be some higher-level construct available in Clojure’s core libraries by now…
It also occurred to me in this chapter that in Clojure, the equivalent of a Ruby block is essentially writing the ~@body
part of a macro. Strictly speaking, you could consider an ordinary function to be the equivalent of a Ruby block (and you’d be right), but in terms of writing a function that takes a block, I think the closest syntactic match would be to write a macro that takes arguments like [foo bar & body]
, where body
is the “block” of code to be executed in some context. I find it interesting the difference in simplicity between Clojure’s (first-class) functions, and the existence of three different things in Ruby (lambdas, blocks and procs, oh my!) that essentially all cover the same territory.
Chapter 6 (Sections 1-3)
; ex. 1:(slurp "http://preeventualist.org/lost"); ex. 2:(slurp "http://preeventualist.org/lost/searchfound?q=truck"); ex. 3:(slurp "folder/idea-about-hiding-lettuce-in-the-church-chairs.txt"); ex. 4:(slurp "http://your.com/idea-about-hiding-lettuce-in-the-church-chairs.txt"); ex. 5:(let [address"http://preeventualist.org/lost/searchfound?q=truck"](with-open [rdr(clojure.java.io/readeraddress)](filter #(re-find #"pickup"%)(line-seq rdr)))); not as pretty as Ruby's syntax with open-uri, I'm afraid...; ex. 6:; macros can be used to emulate block-like behavior in Clojure(defmacro each-line[rdr[line]&body]`(doseq [~line(line-seq ~rdr)]~@body)); ex. 7:(defmacro yield-thrice[&body]`(dotimes [_#3]~@body)); ex. 8:; demonstration of Ruby block syntax -- not relevant to Clojure; ex. 9:(defmacro double-open[[afilename1, bfilename2]&body](letfn[(filename->reader[filename](-> filenameio/resourceio/fileio/reader))]`(with-open [~a(~filename->reader~filename1)](with-open [~b(~filename->reader~filename2)]~@body))))(double-open[x"idea1.txt", y"idea2.txt"](str (first (line-seq x))" | "(first (line-seq y))))
The Ruby version of the above example uses the readline
function, which returns one line and essentially “keeps track” of where you are in the file/URL/whatever object so that a subsequent call to readline
will return the next line. Clojure has a similar function called read-line
that works on stream objects, however it’s not idiomatic to the functional programming style that Clojure promotes. The line-seq
function returns a lazy sequence of all lines in a reader object, so it’s more idiomatic to perform sequence operations on a line-seq
. Because the sequence is lazy, it’s no less performant than using read-line
because you still aren’t consuming the entire sequence at once unless you need to do so.
; ex. 10:(ns ex.preeventualist(:require[clojure.java.io:asio][clojure.string:asstr])(:import(java.netURLEncoder)))(defn open[pagequery](let [qs(str/join"&"(map (fn [[kv]](java.net.URLEncoder/encode(str k"="v)))query))address(str "http://preeventualist.org/lost/"page"?"qs)](str/split(slurp address)#"--\n")))(defn search[word](open"search"{"q"word}))(defn searchlost[word](open"searchlost"{"q"word}))(defn searchfound[word](open"searchfound"{"q"word}))(defn addfound[your-nameitem-lostfound-atdescription](open"addfound"{"name"your-name, "item"item-lost,
"at"found-at, "desc"description}))(defn addlost[your-nameitem-foundlast-seendescription](open"addlost"{"name"your-name, "item"item-found,
"seen"last-seen, "desc"description}))
Okay, and we’ve finally reached the infamous Dwemthy’s Array!
This is a great example of the strength of metaprogramming in Ruby. _why concocts a Creature class that contains some sorcery that allows you to, when inheriting the class from another, more specific class such as a Dragon class, utilize a more concise and fun format for representing RPG-style attributes.
Clojure also supports metaprogramming, but goes about it in a different way than Ruby does. In Clojure, you can utilize macros to essentially create any kind of syntax you can imagine.
The first example is just the desired syntax, so we could do something like:
; exs. 11-13:(defcreatureDragonlife1340strength451charisma1020weapon939)
Of course, in _why’s words, “This is not metaprogramming yet. Only the pill. The product of metaprogramming.” This next example is the implementation:
; ex. 14:(defmacro defcreature[name &attributes](let [fields(mapvfirst (partition2attributes))values(map second (partition2attributes))record-name(symbol (str name "Record"))]`(do(defrecord ~record-name~fields)(defn ~name [](new ~record-name~@values))))); ex. 15:; (Ruby-specific example about attr_reader); ex. 16; (Ruby example showing the explicit naming of traits for the Creature class...)
Come to think of it, our Clojure version of _why’s Creature class is actually better in that it’s more flexible! _why’s class is set up such that every class that inherits from Creature has 4 traits: life, strength, charisma and weapon. Our defcreature
macro lets you choose what attributes each creature has. If we wanted to, we could redefine our macro to only allow those 4 specific attributes, and that would even simplify our code a bit. This is a good example of the flexibility, power and (relative) simplicity of Clojure’s macros.
In the next example, _why shows us what Ruby does “behind the scenes” with his Creature class. So, here is what our defcreature
macro does behind the scenes when we call (defcreature Dragon ...)
:
; ex. 17:(do (defrecord DragonRecord[lifestrengthcharismaweapon])(defn Dragon[](new DragonRecord13404511020939)))
This template isn’t exactly like that of _why’s Creature class. The main difference is that _why’s Creature template defines all creature “subclasses” to have the same 4 traits – life, strength, charisma and weapon. In contrast, our defcreature
macro lets each creature have its own custom traits, making for more flexible creature creation. This is certainly possible in Ruby, as well, but would perhaps make for a more complicated example than would be digestible in _why’s introduction to metaprogramming in Ruby.
; ex. 18:; same as exs. 11-13
Clojure has an eval
method too, but it operates on a data structure instead of a string:
; ex. 19:(def drgn(Dragon)); is identical to...(def drgn(eval '(Dragon))); or, alternatively...(eval '(def drgn(Dragon))); ex. 20:(print "What monster class have you come to battle? ")(def monster-class(read-line))(eval (read-string(format"(def monster (%s))"monster-class)))(pr monster); ex. 20-1/2:; irb example demonstrating instance_eval and class_eval in Ruby.
Clojure has no equivalent to instance_eval
and class_eval
, as there are no instances or classes in Clojure. In Ruby, instance_eval
and class_eval
are useful for adding functionality to instances and classes “on the fly,” e.g. within an irb session, or perhaps even conditionally within a program – allowing you to do things like modify a character class in a certain way if a player reaches a certain point or does a certain thing. As _why puts it, this ability of code to modify code dynamically at runtime “can be useful and … can be dangerous as well.”
At this point in translating Dwemthy’s Array, we need to implement the hit
and fight
methods. We could do this in a functional style, as part of a “Battle” protocol that would be a part of the defrecord
generated by our defcreature
macro. However, records don’t work too well with mutable state in Clojure, since a record is supposed to be an immutable representation of the state of an “object” at any given time.
While it would certainly be possible to re-do Dwemthy’s Array from scratch in a functional style, this would change the dynamic of the original game in Ruby. Dwemthy’s Array is interesting in that _why designed it to be played in an “open” fashion from irb – you might even call it a “metagame.” The player is the programmer. You create an instance of a creature like the Rabbit, define a “Dwemthy’s Array” of enemy creatures, and then have your creature make the first move on the Array, which starts a chain reaction of battling each creature of the Array in succession.
So, for a faithful translation of this game in the spirit of the original, we would want it to be playable in a REPL environment. While it is possible to implement the necessary methods in a functional style, the object-oriented nature of this kind of game lends itself towards using mutable state via Clojure’s reference types.
Whereas the original Dwemthy’s Array relies on monkey-patching (via method_missing
) to allow the Rabbit to attack the Array directly, to make things cleaner we are implementing this as an ordinary function called attack
, which takes a challenger (e.g. the Rabbit), an attack function (e.g. the bomb *
), and a Dwemthy’s Array as arguments. It’s a little more to type, but it makes for safer and tidier code. (You can chalk this one up as a victory for Ruby if you value aesthetics over safety!) ;)
; ex. 21:(defn creature-name[creature](-> (str (typecreature))(clojure.string/split#"\.")(last)))(defn hit[^clojure.lang.Atomcreature, damage](let [p-up(rand-int (:charisma@creature))recovery(if (= 7(rem p-up9))(/ p-up4)0)](when (pos? recovery)(printf"[%s magick powers up %d!]\n"(creature-name@creature)recovery)(swap!creatureupdate-in[:life]+ recovery)))(swap!creatureupdate-in[:life]- damage)(when (not (pos? (:life@creature)))(printf"[%s has died.]\n"(creature-name@creature))))(defn fight[^clojure.lang.Atomcreature, ^clojure.lang.Atomenemy, weapon](if (not (pos? (:life@creature)))(printf"[%s is too dead to fight!]\n"(creature-name@creature))(do (let [your-hit(rand-int (+ (:strength@creature)(:weapon@creature)))](println "[You hit with"your-hit"points of damage!]")(hitenemyyour-hit))(prn @enemy)(when (pos? (:life@enemy))(let [enemy-hit(rand-int (+ (:strength@enemy)(:weapon@enemy)))](println "[Your enemy hit with"enemy-hit"points of damage!]")(hitcreatureenemy-hit))))))(defn attack[^clojure.lang.Atomchallenger, move, ^clojure.lang.Atomdwary](if (pos? (:life@(first @dwary)))(movechallenger(first @dwary))(do (swap!dwarynext)(if-not @dwary(println "[Whoa. You decimated Dwemthy's Array!]")(printf"[Get ready. %s has emerged.]\n"(creature-name@(first @dwary))))))); ex. 22:(defcreatureRabbitlife10strength2charisma44weapon4bombs3); can't redefine ^; (Clojure needs it for metadata); using "v" instead for the boomerang(defn v[rabbitenemy]"little boomerang"(fightrabbitenemy13))(def divide/)(defmulti / (fn [&args](some number?args)))(defmethod / true[&args](apply divideargs))(defmethod / nil[rabbitenemy]"the hero's sword is unlimited!!"(let [elm10(rem (:life@enemy)10)damage(rand-int (+ 4(* elm10elm10)))](fightrabbitenemydamage)))(defn %[rabbitenemy]"lettuce will build your strength and extra ruffage will fly in the face of your opponent!!"(let [recovery(rand-int (:charisma@rabbit))](println "[Healthy lettuce gives you"recovery"life points!!]")(swap!rabbitupdate-in[:life]+ recovery)(fightrabbitenemy0)))(def multiply*)(defmulti * (fn [&args](some number?args)))(defmethod * true[&args](apply multiplyargs))(defmethod * nil[rabbitenemy]"bombs, but you only have three!!"(if (zero? (:bombs@rabbit))(println "[UHN!! You're out of bombs!!]")(do (swap!rabbitupdate-in[:bombs]- 1)(fightrabbitenemy86)))); ex. 22-1/2:(def r(atom(Rabbit)))(:life@r)(:strength@r); ex. 23:(defcreatureScubaArgentinelife46strength35charisma91weapon2); ex. 23-1/2 (several irb snippets):(def r(atom(Rabbit)))(def s(atom(ScubaArgentine)))(vrs)(/ rs)(%rs)(* rs); Ruby example of overloading math operators to do useful ; array functions... Clojure doesn't do this, but here are; the equivalent functions for doing these things:; "array + array"(concat ["D""W""E"]["M""T""H""Y"]); "array - array" (removes all instances of items from the; 2nd array from the 1st array; order is flipped in Clojure)(remove #(some (set %)[\W\T])["D""W""E""M""T""H""Y"]); "array * times" (repeats array "times" times)(flatten(repeat 3["D""W"])); ex. 24:(defcreatureIndustrialRaverMonkeylife46strength35charisma91weapon2)(defcreatureDwarvenAngellife540strength6charisma144weapon50)(defcreatureAssistantViceTentacleAndOmbudsmanlife320strength6charisma144weapon50)(defcreatureTeethDeerlife655strength192charisma19weapon109)(defcreatureIntrepidDecomposedCyclistlife901strength560charisma422weapon105)(defcreatureDragonlife1340strength451charisma1020weapon939); ex. 25:(def dwary(atom(mapvatom[(IndustrialRaverMonkey)(DwarvenAngel)(AssistantViceTentacleAndOmbudsman)(TeethDeer)(IntrepidDecomposedCyclist)(Dragon)]))); ex. 26 ("Start here"):(attackr%dwary); ex. 27:(loop [](print ">> ")(flush)(println "=>"(eval (read-string(read-line))))(recur))
How would you have done Dwemthy’s Array differently? Comments welcome!