(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.
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:
ReplyDelete(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