Deriving a query language using macros

To be sure, we could refactor the preceding lookup function again and shrink it to a manageable size. In fact, implementing solutions using first-class functions is the preferred approach in 9 out of 10 cases. However, it's useful to know other approaches for the proverbial 10th case. With the preceding function-based approach understood, there are other ways of implementing syntaxes, which are otherwise known as Domain-specific Languages (DSLs). The most popular approach in Lisp is using a macro.

Since Clojure syntax is made up of data (homoiconic), we can transform it similar to any other piece of data flowing through our system. Macros are facilities that let us perform this code transformation. In this case, it can help us better design a syntax interface. One thing to be aware of is that the evaluation of inputs (that is, expressions) to your macro is deferred. You have complete control as to if and when they will be evaluated based on the nature of the syntax you provide:

(defn apply-juxt-helper [lookupfn-fns]
  (apply concat ((apply juxt lookupfn-fns)
                 files)))

(defn choose-constraint [mode files constraint-pairs]

  (if (= :or mode)

     (->> (quote ~constraint-pairs)

          (map (fn [x#]
                 [(find-lookup-fn (first x#)) (second x#)]))

          (map (fn [x#]
                 (fn [y#]
                   (lookupfn y# ((first x#) (second x#))))))

          (apply-juxt-helper))

     (->> (quote ~constraint-pairs)

          (map (fn [x#]
                 [(find-lookup-fn (first x#)) (second x#)]))

          (map (fn [x#]
                 ((first x#) (second x#))))

          (fn [x#]
            (fn [y#]
              (every? (fn [pfn#]
                        (pfn# y#))
                      x#)))

          (lookupfn files))))

(defmacro lookup-combined [mode & constraints]
  {:pre [(even? (count constraints))]}

  (let [files (generate-input-list constraints)
        constraint-pairs (generate-constraint-pairs constraints)]

    (choose-constraint mode files constraint-pairs)))

I've changed the syntax slightly to allow for the logical mode to be passed in as the first argument. But the preceding macro that we've created looks similar to the previous lookup functions we made. I've introduced the ->> thrush (recall that it passes each output to the last parameter of the subsequent form) to streamline the let code. The OR and AND blocks are now slightly cleaned up. As stated earlier, though, the evaluation model is different. The body of the macro, including the call out to choose-constraint (apply-juxt-helper is just a helper function that's needed to fit with the ->> thrush pattern), is evaluated because the forms are there. What's interesting, though, is that in the body of choose-constraint, we return one of two possible forms based on the mode that's passed in. Either form is the code that ultimately gets returned from the macro expansion, then evaluated, and ultimately returned from the macro.

Indeed, Clojure provides the macroexpand-1 function that performs this very task. It expands the macro down one level (not expanding all nested macros and expressions), showing the code that is returned, which would ultimately get evaluated by your REPL. So, if we macro expand lookup-combined using the :or mode, it will look a bit different from a macro expanded lookup-combined using the :and mode. While doing this is possible, it isn't meant to be read so much as it is a demonstration of how your code is transformed and the difference produced for each option:

(macroexpand-1 '(lookup-combined :or :time-after #inst "2015-08-15T17:18:00.000-00:00" :price-abouve 20))

(clojure.core/->>
 '((:time-after #inst "2015-08-15T17:18:00.000-00:00")
   (:price-abouve 20))
 (clojure.core/map
  (clojure.core/fn
   [x__27781__auto__]
   [(edgaru.eight/find-lookup-fn (clojure.core/first x__27781__auto__))
    (clojure.core/second x__27781__auto__)]))
 (clojure.core/map
  (clojure.core/fn
   [x__27781__auto__]
   (clojure.core/fn
    [y__27782__auto__]
    (edgaru.eight/lookupfn
     y__27782__auto__
     ((clojure.core/first x__27781__auto__)
      (clojure.core/second x__27781__auto__))))))
 (edgaru.eight/apply-juxt-helper))

(macroexpand-1 '(lookup-combined :and :time-after #inst "2015-08-15T17:18:00.000-00:00" :price-abouve 20))

(clojure.core/->>
 '((:time-after #inst "2015-08-15T17:18:00.000-00:00")
   (:price-abouve 20))
 (clojure.core/map
  (clojure.core/fn
   [x__27783__auto__]
   [(edgaru.eight/find-lookup-fn (clojure.core/first x__27783__auto__))
    (clojure.core/second x__27783__auto__)]))
 (clojure.core/map
  (clojure.core/fn
   [x__27783__auto__]
   ((clojure.core/first x__27783__auto__)
    (clojure.core/second x__27783__auto__))))
 (clojure.core/fn
  [x__27783__auto__]
  (clojure.core/fn
   [y__27784__auto__]
   (clojure.core/every?
    (clojure.core/fn
     [pfn__27785__auto__]
     (pfn__27785__auto__ y__27784__auto__))
    x__27783__auto__)))
 (edgaru.eight/lookupfn edgaru.eight/files))

Now we can use the finalized lookup-combined macro to execute in either logical AND or OR mode:

(count (lookup-combined :or :time-after #inst "2015-08-15T17:18:00.000-00:00" :price-abouve 20))
;; 2126

(count (lookup-combined :and :time-after #inst "2015-08-15T17:18:00.000-00:00" :price-abouve 20))
;; 1915

Let's try a slightly more data-oriented query syntax where we pass in a vector of conditions. This is easily deconstructed in the first let block of the macro:

(defmacro lookup [query-params]

  ;; ensure constraints are in pairs -> Preconditions
  {:pre [(even? (count (rest query-params)))]}

  ;; map over pairs - find predicate fn based on keyword - partially apply fn with arg
  (let [mode (first query-params)
        constraints (rest query-params)
        files (generate-input-list constraints)
        constraint-pairs (generate-constraint-pairs constraints)]

    (choose-constraint mode files constraint-pairs)))

But an approach where the query syntax body itself can become data allows for queries to be stored in separate locations. Now we can execute lookups like this:

(count (lookup [:or :time-after #inst "2015-08-15T17:18:00.000-00:00" :price-abouve 20]))
;; 2126

(count (lookup [:and :time-after #inst "2015-08-15T17:18:00.000-00:00" :price-abouve 20]))
;; 1915

These are very rudimentary initial steps. We may choose to expand our syntax to include negation or nested AND/OR conditions. Since the evaluation of inputs (that is, expressions) to your macro is deferred, we can pass in constraint expressions instead of values and control if and when these expression paths are chosen.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.189.186.109