Friday, October 16, 2009

Beginning Clojure Macros - Part 2

Last time, we went over what macros were, and their parts. I also gave a pretty poor example of how to use them. I'll be taking that same code and being a little more efficient about it.

(defn parse-ip [ip current]
(if
;;; probably should use the network lib to be as accurate as possible...
;;; but I won't
(re-matches #"[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+" ip)
(inc-summary-counter current ip)
current))

(defn parse-datetime [ datetime current ]
(if
;;; since we're splitting on spaces, we get back "[19/Jan/2006:04:30:26"
(re-matches #"\[.{20}" datetime)
(inc-summary-counter current (subs datetime 1 12))
current))
There's a lot of repetition there. It's a simple if statement, but it will do for this example.


(defmacro parse-field [ valid-expr update-expr skip-expr ]
`(if
~valid-expr
~update-expr
~skip-expr))

Awesome! We just re-created if! Let's use it...


(let [[chash ctext] (get-field results :path-summary 6 nline)]
(parse-field
(re-matches #"/.+" ctext)
(inc-summary-counter chash ctext)
chash))

At least that's more self-explantory than a commented if-statement. Let's sort-of expand this.


(let [[chash ctext] (get-field results :path-summary 6 nline)]
;;; replacement of "parse-field" begins here
(if
;;; ~valid-expr
(re-matches #"/.+" ctext)
;;; ~update-expr
(inc-summary-counter chash ctext)
;;; ~skip-expr
chash))

Yep. That's it. Effectively, the only thing that changed was parse-field into if, right? Yes, but rather than evaluate the expressions, then pass them, we're passing the expressions themselves. They don't get evaluated for their values until they are required. Clojure already does things quite lazily, and caches a lot, but it can't anticipate everything, such as a long-running database call. It sometimes is just better to build in assurances yourself, anyway.

Why not a function?


Another way to achieve the same thing would've been to pass anonymous functions to another function. This would achieve the deferral of expensive evaluations, encapsulation, expressiveness, etc., as well, thanks to the power of closures:

(defn parse-field-fn [ current-hash current-text valid-fn update-fn ]
(if
(valid-fn current-text)
(update-fn current-hash current-text)
current-hash))

(let [[chash ctext] (get-field results :path-summary 6 nline)]
(parse-field-fn
chash
#(re-matches #"/.+" ctext )
#(inc-summary-counter chash ctext)))


When should I use macros instead of functions?


You should look for opportunities any time you're writing a function to replace structure. I only used a single if statement, but it could have had several. Another good opportunity is to replace a complicated function. If you have multiple nested if statements, conds, whatever, the main code can be easier to read by using a macro.

In all, though, learning macros - and any metaprogramming techniques - is worthwhile. It gives you greater control assembling the various bits and pieces which make up the program.

1 comment:

  1. good intro post to macros, i think in them like ports to alter the language syntax. Macros lets you abstract or mould the syntax (or code structure) like destructuring in let and other macros, another feature that shocks me the first time i see:

    (let [{j :j, k :k, i :i, [r s & t :as v] :ivec, :or {i 12 j 13}}
    {:j 15 :k 16 :ivec [22 23 24 25]}]
    [i j k r s t v])
    -> [12 15 16 22 23 (24 25) [22 23 24 25]]

    Example taken from
    http://clojure.org/special_forms

    ReplyDelete