Clojure Macro Explorations: cond->
Some notes I’ve taken on my journey to really understand Clojure macros. Today I basically walked through the cond-> macro in clojure.core, and these are my notes as I figured out how it works.
;; cond-> from clojure/core.clj
;;
;; This is one of the easier to understand threading macros in the source code, even
;; if it’s use isn’t immediately obvious. However, I’m working on figuring out how
;; to really leverage macros, and the best examples are in the source code.
;;
;; cond-> is a variation of the thread-first function ->, which executes a sequence
;; of forms, while passing in the previous value as the first argument.
;;
;; For example, the following code:
;; (-> 1 inc inc (+ 3))
basically expands to something equivelent to:
;;
1 2 3 4 5 |
;; (let [x 1 ; Initial value ;; x (inc x) ; pass prev x as 1st param to inc ;; x (inc x) ; again ;; x (+ x 3)] ; then pass x into (+ 3) giving (+ x 3) ;; x) ; Finally, return the last (final) value of x |
;; Pedantic note: This really expands to (+ inc (inc 1)) 3), but I
;; think the above is easier to understand, and seques better.
;;
;; The cond-> macro does something very similar, but it allows
;; putting GUARDS on each step of the computation; if the GUARD is
;; true, then that step executes, else it is skipped.
;;
;; One thing to node is that the GUARD can NOT access the current
;; value of the computation. Thus, the use of cond-> seems to be
;; something like CASE statements where multiple branches can be chained.
;;
;; Let’s compare -> to cond-> in terms of expansion…
1 2 3 4 5 6 |
;; (-> 1 inc (+ 3)) (cond-> 1 true inc false (+ 3)) ;; ;; (let [x 1 (let [x 1 ;; x (inc x) x (if true (inc x) x) ;; x (+ x 3)] x (if false (+ x 3) x)] ;; x) x) |
;; Keep an eye on the above right expansion as you browse the macro:
;;
;; G in the macro is x above
;; initial in the macro is 1 above
;; pstep is the function that creates the (if X (f x) x)
code.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
(defmacro my-cond-> [initial & clauses] (assert (even? (count clauses))) ;; When we start, we aren't really in the scary macro part yet. (let [;; G is the temporary variable that will be used in the macro steps. G (gensym) ;; pstep is a function that takes a test and a block of code to execute ;; if the test is true, and macrowinds it. Basically it's a function to ;; create an expanded macro. ;; ;; For example: ;; (pstep true (+ 3)) -> '(if true (-> (+ G 3))) pstep (fn [[test step]] `(if ~test (-> ~G ~step) ~G))] ;; Now entring the primary part of the macro. ;; ~G pastes the expansion of (gensym); more or less the same as ;; if we defined it as g#, except that G is available outside the ;; macro. But that's not important in this particular case. `(let [~G ~initial ;; We're still in the let binding here, and we use the above "pstep" to ;; "paste" in the generated code. ;; ;; interleave takes a pair of lists and makes a new one by mixing them. ;; e.g. (interleave [:a :b] [3 1]) => [[:a 3] [:b 1]] ;; ;; So it's basically pasting in the temp var G with each of the expressions, like this: ;; let [~G ~initial ;; ~G first-expansion of pstep ;; ~G second-expansion of pstep ... ;; ;; Recall above, pstep takes two parameters. The (partition 2 clauses) breaks ;; up the "clauses" into pairs, so they can be fed to the "pstep" call by map. ~@(interleave (repeat G) (map pstep (partition 2 clauses)))] ;; Finally, the above let block has completed, each step re-setting the value of G, ;; so the last step has the final value, and it gets returned here. ~G))) |
Leave a Reply