Ваша формулировка проблемы кажется немного расплывчатой, поэтому я решу упрощенную версию проблемы.
Имейте в виду, что макрос - это механизм перевода кода . То есть он переводит код, который вы sh вы могли бы написать во что-то, что приемлемо для компилятора . Таким образом, лучше всего думать о результате как о расширении компилятора . Написание макроса сложно и почти всегда не нужно . Так что не делайте этого, если вам это действительно не нужно.
Давайте напишем предикат помощника и модульный тест:
(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)))
Предположим, мы хотим перевести этот код:
(check-> 10
(+ 3)
(* 100)
(century?) )
в это:
(-> 10
(+ 3)
(* 100)
(if (century) ; <= arg goes here
:pass
:fail))
Переписать цель немного:
(let [x (-> 10 ; add a temp variable `x`
(+ 3)
(* 100))]
(if (century? x) ; <= use it here
:pass
:fail))
Теперь начнем с функции -impl
. Напишите немного, с некоторыми заявлениями для печати. Внимательно обратите внимание на шаблон для использования:
(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
и проведите его с помощью модульного теста. Обязательно следуйте схеме оборачивания аргументов в цитируемом векторе . Это моделирует то, что [& args]
делает в выражении defmacro
.
(dotest
(pprint/pprint
(check->-impl '[10
(+ 3)
(* 100)
(century?)])
))
с результатом:
args => [10 (+ 3) (* 100) (century?)] ; 1 (from spyx)
[10 (+ 3) (* 100) (century?)] ; 2 (from pprint)
Таким образом, мы видим результат, напечатанный в (1), затем функцию impl возвращает (неизмененный) код в (2). Это ключ. Макрос возвращает модифицированный код . Затем компилятор компилирует измененный код вместо исходного.
Напишите еще немного кода с большим количеством отпечатков:
(defn check->-impl
[args] ; no `&`
(let [all-but-last (butlast args)
last-arg (last args) ]
(spyx all-but-last) ; (1)
(spyx last-arg) ; (2)
))
с результатом
all-but-last => (10 (+ 3) (* 100)) ; from (1)
last-arg => (century?) ; from (2)
(century?) ; from pprint
Обратите внимание, что произошло , Мы видим наши измененные переменные, но результат также изменился. Напишите еще немного кода:
(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
Упс! Функция append
всегда возвращает вектор. Просто используйте ->list
, чтобы преобразовать его в список. Вы также можете набрать (apply list ...)
.
cond-expr => (century? x) ; better
Теперь мы можем использовать синтаксическую кавычку для создания кода нашего выходного шаблона:
(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))))
с результатом:
(clojure.core/let
[tst.demo.core/x (clojure.core/-> 10 (+ 3) (* 100))]
(if (century? x) :pass :fail))
Видите часть tst.demo.core/x
? Это проблема. Нам нужно переписать:
(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))))
ПРИМЕЧАНИЕ. Важно правильно использовать ~
(eval) и ~@
(eval-splicing). Легко ошибиться. Теперь мы получаем
(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))
Попробуйте по-настоящему. Разверните аргументы из вектора в кавычках и вызовите макрос вместо функции impl:
(spyx-pretty :final-result
(check-> 10
(+ 3)
(* 100)
(century?)))
с выводом:
:final-result
(check-> 10 (+ 3) (* 100) (century?)) =>
:pass
и напишите несколько модульных тестов:
(dotest
(is= :pass (check-> 10
(+ 3)
(* 100)
(century?)))
(is= :fail (check-> 10
(+ 3)
(* 101)
(century?))))
с результатом:
-------------------------------
Clojure 1.10.1 Java 13
-------------------------------
Testing tst.demo.core
Ran 3 tests containing 4 assertions.
0 failures, 0 errors.
Вы также можете быть заинтересованы в этой книге: Мастеринг макросов Clojure