In these posts I’m studying the book Practical Common Lisp by Peter Siebel and coding the examples in Clojure. Aim: studying clojure and reading this fantastic book can be accessed online here.
In part 2 of this post, we implemented select, where and update function for our documentish db. But the implementation had fair bit of code duplication. We remove code duplication and make the code more generic in this section.
Chapter 3 continued
Removing duplication
In this section, Peter highlights that last sections where implementation was specific to the given use case (documents corresponding to cd) and had fair bit of code repeatation. He solves both problems with a re-write of where as a macro with two helper functions.
My clojure code is as given below, the explanation follows.
Single comparison expression
In a database query, there could be multiple where clauses/conditions. For each clause, we would need a comparison expression. The following code generates such expression for a passed in field and value. For reasons little difficult for me to digest right now, there’s a reference to a dynamic var row in the implementation. We will discuss this point later. Note for now that the function gives different result a new binding of row var.
(def ^:dynamic row nil)
(defn make-comparison-expr
[[field value]]
(list '= value (list 'get row field)))
(make-comparison-expr '(:rating 8))
(binding [row {:rating 8 :artist "Someone"}]
(make-comparison-expr '(:rating 8)))
Multiple comparison expressions
What if there are multiple clauses/conditions in where? We iterate over each pair and build a list containing each comparison expression. Since the passed in list is a sequence of field and value, we use partition to retrieve pairs.
;; for multiple pairs, make expression for each
(defn make-comparison-expr-list
[all-field-pairs]
(let [pairs (partition 2 all-field-pairs)]
(map #(make-comparison-expr %) pairs)))
(binding [row {:rating 8 :artist 9}]
(make-comparison-expr-list '(:artist "Dixie Chicks" :rating 8)))
The where macro
Now comes the hairy part. Here we want to write where as macro, which returns the right predicate generator. To be honest, I struggled with this part a lot. There are multiple reasons,
- The macro is returning an anonymous function. The anonymous function takes an argument (the row/document in the database). Not covered in many macro tutorials/training :-P
- The and in Clojure itself is a macro which takes expressions and not a list of expressions. Whereas our make-comparison-expr-list is returning a list. We can use ~@ to splice the list, but I am staying clear of it for now.
- The reference to the row needed for make-comparison-expr. I thought the best way would be to pass in a symbol. So make-comparison-expr function should also take in row alongwith field, value pair. I couldn’t get this to work.
Although I could write the two functions in the last subsection myself, the macro simply too hard for me at this point. So I needed some help from Stuart Halloway’s solution here. I solved problem by appending a pound/hash to the row argument to the anonymous function. My incomplete understanding tells me that it generates a new symbol for the anonymous function argument. Variable capture and what not. I need to study this later. For problem 2, I used every? function with eval on each return value from make-comparison-expr-list rather than splice unquote. For problem 3, I still don’t understand why we need a global reference to the row which is bound again inside the macro. I do know that getting the macro to work is littled difficult otherwise.
(defmacro where [clauses]
`(fn [cd#]
(binding [row cd#]
(every? eval (make-comparison-expr-list ~clauses)))))
(defn select [wherefunc]
(filter wherefunc (deref db)))
(select (where '(:title "Fly")))
Learning So Far
- Treating code as data, wow! We generated the expression for comparison depending upon the field and value passed in. I’ve not seen this done in any other language so succinctly.
- Treating code as data helped us to make the functions more generic. Peter Norvig’s lisp guide highlighted this explicitly and so did SICP book: make your functions generic so that they can be re-used. Saw this in action.
- First rule of macro club is there for a reason! Stay clear of macros as long as you don’t understand them well.
This concludes chapter 3. Onwards!