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,
cond
s, 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.