clojuremacrossyntax-rules

How clojure macro parses special symbol?


When I re-implement a macro written in Scheme with Clojure, I get into a trouble. The macro tries to load pairs of testing data into a all-tests var for later use.

Because the arguments for the macro is variable-length and contains special undefined symbol, i.e. =>, I simply don't know how to parse it like what Scheme syntax-rules does.

Scheme Version:

(define all-tests '())

;;; load tests into all-tests
(define-syntax add-tests-with-string-output
  (syntax-rules (=>)
    [(_ test-name [expr => output-string] ...)
     (set! all-tests
        (cons 
           '(test-name [expr string  output-string] ...)
            all-tests))]))

(add-tests-with-string-output "integers"      
  [0  => "0\n"]                    
  [1  => "1\n"]                    
  [-1 => "-1\n"]                   
  [10  => "10\n"]                    
  [-10 => "-10\n"]                   
  [2736 => "2736\n"]               
  [-2736 => "-2736\n"]             
  [536870911 => "536870911\n"]     
  [-536870912 => "-536870912\n"]   
)

My current unsuccessful Clojure Version:

(def all-tests (atom '()))

(defmacro add-tests-with-string-output
  [test-name & body]
  `(loop [bds# (list body)]
    (when-not (empty? bds#)
      (println (first bds#))
      (recur (rest bds#)))))

Ps: I am using println to test my code right now. When it works, I will try to do the parsing and loading work.


Solution

  • The first macro forms a loop and the second one a doseq (so is simpler). Both should behave the same. Also I find it a good idea to extract as much logic out of macros into auxiliary functions. Functions are easier to debug, test and write. If the macro were a bit more complicated I might have left even less logic in it.

    (def all-tests (atom '()))
    
    (defn add-test [test-name expr output-string]
      (swap! all-tests #(cons (list test-name [expr output-string]) %)))
    
    (defmacro add-tests-with-string-output
      [test-name & body]
      ;`(loop [bds# '(~@body)]
      `(loop [bds# '~body] ; edit
        (when-not (empty? bds#)
          (let [bd# (first bds#)
                expr# (first bd#)
                output-string# (last bd#)]
            (add-test ~test-name expr# output-string#)
            (recur (rest bds#))
            ))))
    
    (defmacro add-tests-with-string-output2
      [test-name & body]
      ;`(doseq [bd# '(~@body)]
      `(doseq [bd# '~body] ; edit
        (let [expr# (first bd#)
              output-string# (last bd#)]
          (add-test ~test-name expr# output-string#))))
    
    user=> (add-tests-with-string-output "test1" [0  => "0\n"] [1  => "1\n"])
    nil
    user=> (add-tests-with-string-output2 "test2" [0  => "0\n"] [1  => "1\n"])
    nil
    user=> @all-tests
    (("test2" [1 "1\n"]) ("test2" [0 "0\n"]) ("test1" [1 "1\n"]) ("test1" [0 "0\n"]))