Parts 1, 2 and 3 of this series can be found here, here and here.
So here’s where _why starts getting into monkey-patching, which I would consider to be a key feature of Ruby… sure, it’s dangerous in that it has the potential to seriously break your code, but Ruby lives on the edge! Monkey-patching in Ruby is kind of interesting in that Rubyists always warn you not to do it, while at the same time showing off how cool and powerful it is. Clojure, on the other hand, is set up in such a way that it’s really difficult to majorly screw things up. Everything is namespaced, so functions can’t really override each other, and while technically it is possible to override core functions by either redefining them or extending protocols, the temptation to do that really isn’t there, thanks to Clojure providing easier ways to go about solving the problem. Namely, multimethods provide an easy and surprisingly flexible avenue for polymorphism. In fact, multimethods actually one-up polymorphism in that you can dispatch based not just on the type of the arguments, but on any function! In this chapter I decided to translate the monkey-patching examples mostly just by defining ordinary functions that assume the argument you’re passing in is of a particular type. When we get to Dwemthy’s Array (which is in the next chapter, I think?), you’ll see me take the multimethod approach to do that crazy thing where _why monkey-patched a handful of math operators to make them double as weapons used by a rabbit. (If you haven’t read _why’s (Poignant) Guide to Ruby, you probably have no idea what I’m talking about!)
OK, enough yammering. Onward!
Chapter 5 (Sections 4-7)
The kind of monkey-patching involved in the next example isn’t really available in Clojure… The way to do it would be to create a new protocol/record that takes an ordinary array as an argument and implements this modified join
function on the array instead of e.g. clojure.string/join
.
The irb examples here present another thing in Ruby that you can’t really do in Clojure, or at least it isn’t idiomatic. You can access all of the above objects from a different namespace as ex.watchful-saint-agnes/FatWaxyChild
, ex.watchful-saint-agnes/timid-foxfaced-girl
, etc. The idea of “extending” a new “class” to include “copies” of these objects doesn’t really make sense within Clojure’s functional paradigm. Needless to say, this is because Clojure doesn’t have OO-style classes.
Ex. 40 explains what attr_accessor
does in a Ruby class; in this case, you can use attr_accessor :picks, :purchased
as a shortcut for def picks; @picks; end; def purchased; @purchased; end
. Clojure records make things even simpler in that there is no need to explicitly declare getter methods. Because records function just like maps, getting the fields is as easy as getting the value from a map:
Exs. 49-50 showcases ||=
, a Ruby trick used here to “initialize” an entry in a map to an empty array []
if there is not already a key with a certain name in said map. This is not needed in Clojure, as you can just do, e.g., (merge-with f map {key val})
and if the map already contains the key, it will “update” it by calling the function on the existing value, otherwise it will “create” the {key val} entry you are merging in.
Re: this particular example of conditional assignment (winners[buyer] = winners[buyer] || [])
, Clojure essentially has this “built into” its implementation of hash-maps. If you try to look up a key in a map that doesn’t contain said key, you’ll conveniently get nil
.
The next example references the MindReader read
method from ex. 13. In this Ruby example, _why refers to self.read
within a module, with the intent of mixing the module into an existing class that contains a read
method, such as the MindReader class from before. To emulate this in Clojure, we can create an ordinary function that takes a this
argument (a record).
There is actually no need to “mix in” this function to our existing MindReader record; as an ordinary function, it can be used by (or rather, on) any record that implements a protocol that has a read
method. If you pass in a MindReader record to the scan-for-a-wish
method, it will replace the this
in (read this)
, and the correct read
method will be dispatched.
…This example doesn’t really work within Clojure’s functional programming paradigm. To be fair, it isn’t properly explained how the Ruby example’s infinite loop goes on to the next wish after the last one has been granted. I assume the WishMaker’s grant
method would destructively delete the wishful thought from the queue of thoughts read by the MindReader, so that on the next iteration of the loop, the scan_for_a_wish
method will find a new wish. If this were an actual program in Clojure, scanning thoughts for wishes in real-time, we would probably want to use a ref or an atom to represent a (lazy?) sequence of thoughts, and have an infinite loop that checks the first thought and either grants it (if it starts with “wish: “) or discards it, updating the ref or atom’s state with each iteration of the loop.