Как clojure макрос разбирает спецсимвол?

Когда я заново внедряю макрос, написанный на Scheme с Clojure, я попадаю в беду. Макрос пытается загрузить пары тестовых данных в all-tests Вар для последующего использования.

Потому что аргументы для макроса имеют переменную длину и содержат специальный неопределенный символ, т.е. =>Я просто не знаю, как его анализировать, как это делают правила синтаксиса Scheme.

Версия схемы:

(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"]   
)

Моя текущая неудачная версия Clojure:

(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: я использую println чтобы проверить мой код прямо сейчас. Когда это сработает, я постараюсь выполнить разбор и загрузку.

2 ответа

Решение

Первый макрос образует цикл, а второй - doseq (так проще). Оба должны вести себя одинаково. Также я считаю хорошей идеей извлекать как можно больше логики из макросов во вспомогательные функции. Функции легче отлаживать, тестировать и писать. Если бы макрос был немного сложнее, я бы оставил в нем еще меньше логики.

(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"]))

После проб и ошибок наконец-то выясняю, как ее решить.

Сначала используйте Destructuring для решения аргументов переменной длины; позже не используйте синтаксические кавычки, т.е. обратные кавычки `, внутри макроса, потому что если это так, однажды вам нужно снять кавычки ~ аргумент, т.е. body, вы получите сообщение об ошибке, как это из-за специального символа =>:

CompilerException java.lang.RuntimeException: невозможно разрешить символ: => в этом контексте

Ниже мое решение. Если вам лучше или вы знаете причину, по которой синтаксические цитаты и Unquote работают неправильно, пожалуйста, дайте мне знать.

;;; load tests into all-tests
(def all-tests (atom '()))
(defmacro add-tests-with-string-output
  [test-name & body]
  (loop [bds body, tests '()]
    (if (empty? bds)
      (do 
        (swap! all-tests #(cons (cons test-name tests) %))
        nil)
      (let [pair (first bds),
            input (first pair)
            output (last pair)]
        (recur (rest bds) (cons (list input ''string output) tests))))))
Другие вопросы по тегам