Quantcast
Channel: dave yarwood
Viewing all articles
Browse latest Browse all 64

_why's (Poignant) Guide to Ruby in Clojure: Part 4

$
0
0

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)

; exs. 32-34:
; not applicable to Clojure (Ruby class inheritance)
; although this is somewhat similar to implementing protocols in records
; ex. 35:
(defnmail-them-a-kit[address](when-not(instance?addressex.Address)(throw(IllegalArgumentException."No Address object found.")))(print(formattedaddress)))

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.

; ex. 36:
(defprotocolArrayPlus(join[a][asep][asepfrmt]"Joins strings together into a formatted string."))(defrecordArrayMine[a]ArrayPlus(join[am](joinam""))(join[amsep](joinamsep"%s"))(join[amsepfrmt](clojure.string/joinsep(map#(formatfrmt%)a)))); ex. 37:
(defrooms(ArrayMine.[346]))(str"We have "(joinrooms", ""%d bed")" rooms available."); ex. 38:
; Ruby modules are analagous to Clojure namespaces
(nsex.watchful-saint-agnes)(defToothlessManWithFork["man""fork""exposed gums"])(defrecordFatWaxyChild[])(deftimid-foxfaced-girl{"please""i want an acorn please"})

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. 39:
(defrecordLotteryTicket[pickspurchased])(defnnew-lottery-ticket[&picks](cond(not=(countpicks)3)(throw(IllegalArgumentException."three numbers must be picked"))(not=(count(distinctpicks))3)(throw(IllegalArgumentException."the three picks must be different numbers"))(not-every?#(some#{%}(range126))picks)(throw(IllegalArgumentException."the three picks must be numbers between 1 and 25")))(LotteryTicket.picks(java.util.Date.))); ex. 40
;(explanationofattr_accessor)

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:

; ex. 41:
(defticket(applynew-lottery-ticket(repeatedly3#(rand-nth(range126)))))(:picksticket); ex. 42:
; unlike in Ruby, this works by default
(assocticket:picks[2619]); ex. 43:
; in Clojure it would make more sense for this to be an ordinary function,
; not a class method belonging to the LotteryTicket "class"(defnrandom-lottery-ticket[](applynew-lottery-ticket(repeatedly3#(rand-nth(range126))))); results in an IllegalArgumentException if not all 3 numbers are unique
; ex. 44:
(defnrandom-lottery-ticket[](try(applynew-lottery-ticket(repeatedly3#(rand-nth(range126))))(catchIllegalArgumentExceptione(random-lottery-ticket)))); recursively calls itself until it returns a valid lottery ticket (3 unique #'s)
; ex. 45:
; rather than making this all part of a "LotteryDraw class," it would be more
; idiomatic in Clojure to include all of these as top-level objects and functions
; in a namespace called, say, (ns ex.lottery-draw)
(deftickets(atom{}))(defnbuy[customer&tickets](swap!tickets#(merge-withconcat%{customertickets}))); ex. 46:
(buy"Yal-dal-rip-sip"(new-lottery-ticket12619)(new-lottery-ticket513)(new-lottery-ticket2468)); ex. 47:
(defprotocolScoring(score[ticketfinal]"Counts the number of correct numbers on the ticket."))(extend-typeLotteryTicketScoring(score[ticketfinal](count(clojure.set/intersection(set(:picksticket))(set(:picksfinal)))))); ex. 48: 
(defnplay[]"Returns a hash of each winner to a list of their winning tickets, in the
   form {winner ([ticket1 score1] [ticket2 score2])}."(let[winning-numbers(random-lottery-ticket)](into{}(for[[plyrtkts]@tickets:when(some#(>(score%winning-numbers)0)tkts)][plyr(for[tkttkts:let[score(scoretktwinning-numbers)]:when(>score0)][tktscore])]))))

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.

; ex. 50-1/2:
; (this is an irb example that could just as easily be a standalone script)
(doseq[[winnertickets](play)](printlnwinner"won on"(counttickets)"ticket(s)!")(doseq[[ticketscore]tickets](println(str"\t"(clojure.string/join", "(:picksticket))": "score)))); exs. 51-52:
; not applicable to Clojure, wherein records work just like hash-maps
; ex. 53:
(defprotocolJudging(the-winner[contestname]))(defrecordSkatingContest[winner]Judging(the-winner[_name](when-not(string?name)(throw(IllegalArgumentException."The winner's name must be a String,
                not a math problem or a list of names or any of that business.")))(SkatingContest.name))); example usage:
(defcontest(SkatingContest.nil))(the-winnercontest"Dave");=> (SkatingContest. "Dave")
; not exactly something you would do in Clojure, but it's possible!
; ex. 54:
(defNOTES[:Ab:A:Bb:B:C:Db:D:Eb:E:F:Gb:G])(defrecordAnimalLottoTicket[pickspurchased])(defnnew-ticket[note1note2note3:asnotes](when-not(distinct?notes)(throw(IllegalArgumentException."the three picks must be different notes")))(letfn[(valid-note?[x](some#{x}NOTES))](when-not(every?valid-note?notes)(throw(IllegalArgumentException."the three picks must be notes in the chromatic scale."))))(AnimalLottoTicket.notes(java.util.Date.)))(defnrandom-ticket[](try(applynew-ticket(repeatedly3#(rand-nthNOTES)))(catchIllegalArgumentExceptione(random-ticket))))(defprotocolScoring(score[ticketfinal]"Counts the number of correct numbers on the ticket."))(extend-typeAnimalLotteryTicketScoring(score[ticketfinal](count(clojure.set/intersection(set(:picksticket))(set(:picksfinal))))))

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).

; ex. 55:
(require'[endertromb.core:asendertromb])(defnscan-for-a-wish[this](when-let[wish((compfirstfilter)#(=(subs%06)"wish: ")(readthis))](clojure.string/replacewish"wish: """)))

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.

; ex. 57:
(defreader(MindReader.(endertromb/scan-for-sentience)))(defwisher(WishMaker.(rand-int6)))...

…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.


Viewing all articles
Browse latest Browse all 64

Trending Articles