Your problem statement seems a little vague, so I'll solve a simplified version of the problem.
Keep in mind that a macro is a code-translation mechanism. That is, it translates the code that you wish you could write into something that is acceptable by the compiler. In this way, it is best to think of the result as a compiler extension. Writing a macro is complicated and almost always unnecessary. So, don't do it unless you really need it.
Let's write a helper predicate and unit test:
(ns tst.demo.core
(:use tupelo.core tupelo.test) ; <= *** convenience functions! ***
(:require [clojure.pprint :as pprint]))
(defn century? [x] (zero? (mod x 100)))
(dotest
(isnt (century? 1399))
(is (century? 1300)))
Suppose we want to translate this code:
(check-> 10
(+ 3)
(* 100)
(century?) )
into this:
(-> 10
(+ 3)
(* 100)
(if (century) ; <= arg goes here
:pass
:fail))
Re-write the goal a little:
(let [x (-> 10 ; add a temp variable `x`
(+ 3)
(* 100))]
(if (century? x) ; <= use it here
:pass
:fail))
Now start on the -impl
function. Write just a little, with some print statements. Notice carefully the pattern to use:
(defn check->-impl
[args] ; no `&`
(spyx args) ; <= will print variable name and value to output
))
(defmacro check->
[& args] ; notice `&`
(check->-impl args)) ; DO NOT use syntax-quote here
and drive it with a unit test. Be sure to follow the pattern of wrapping the args in a quoted vector. This simulates what [& args]
does in the defmacro
expression.
(dotest
(pprint/pprint
(check->-impl '[10
(+ 3)
(* 100)
(century?)])
))
with result:
args => [10 (+ 3) (* 100) (century?)] ; 1 (from spyx)
[10 (+ 3) (* 100) (century?)] ; 2 (from pprint)
So we see the result printed in (1), then the impl function returns the (unmodified) code in (2). This is key. The macro returns modified code. The compiler then compiles the modified code in place of the original.
Write some more code with more prints:
(defn check->-impl
[args] ; no `&`
(let [all-but-last (butlast args)
last-arg (last args) ]
(spyx all-but-last) ; (1)
(spyx last-arg) ; (2)
))
with result
all-but-last => (10 (+ 3) (* 100)) ; from (1)
last-arg => (century?) ; from (2)
(century?) ; from pprint
Notice what happened. We see our modified variables, but the output has changed as well. Write some more code:
(defn check->-impl
[args] ; no `&`
(let [all-but-last (butlast args)
last-arg (last args)
cond-expr (append last-arg 'x)] ; from tupelo.core
(spyx cond-expr)
))
cond-expr => [century? x] ; oops! need a list, not a vector
Oops! The append
function always returns a vector. Just use ->list
to convert it into a list. You could also type (apply list ...)
.
cond-expr => (century? x) ; better
Now we can use the syntax-quote to create our output template code:
(defn check->-impl
[args] ; no `&`
(let [all-but-last (butlast args)
last-arg (last args)
cond-expr (->list (append last-arg 'x))]
; template for output code
`(let [x (-> ~@all-but-last)] ; Note using `~@` eval-splicing
(if ~cond-expr
:pass
:fail))))
with result:
(clojure.core/let
[tst.demo.core/x (clojure.core/-> 10 (+ 3) (* 100))]
(if (century? x) :pass :fail))
See the tst.demo.core/x
part? That is a problem. We need to re-write:
(defn check->-impl
[args] ; no `&`
(let [all-but-last (butlast args)
last-arg (last args)]
; template for output code. Note all 'let' variables need a `#` suffix for gensym
`(let [x# (-> ~@all-but-last) ; re-use pre-existing threading macro
pred-result# (-> x# ~last-arg)] ; simplest way of getting x# into `last-arg`
(if pred-result#
:pass
:fail))))
NOTE: It is important to use ~
(eval) and ~@
(eval-splicing) correctly. Easy to get wrong. Now we get
(clojure.core/let
[x__20331__auto__ (clojure.core/-> 10 (+ 3) (* 100))
pred-result__20332__auto__ (clojure.core/-> x__20331__auto__ (century?))]
(if pred-expr__20333__auto__
:pass
:fail))
Try it out for real. Unwrap the args from the quoted vector, and call the macro instead of the impl function:
(spyx-pretty :final-result
(check-> 10
(+ 3)
(* 100)
(century?)))
with output:
:final-result
(check-> 10 (+ 3) (* 100) (century?)) =>
:pass
and write some unit tests:
(dotest
(is= :pass (check-> 10
(+ 3)
(* 100)
(century?)))
(is= :fail (check-> 10
(+ 3)
(* 101)
(century?))))
with result:
-------------------------------
Clojure 1.10.1 Java 13
-------------------------------
Testing tst.demo.core
Ran 3 tests containing 4 assertions.
0 failures, 0 errors.
You may also be interested in this book: Mastering Clojure Macros