Подтвердить что ты не робот

Множественная arity в defmacro clojure

У меня возникла странная проблема, связанная с defmacro в Clojure, у меня есть код вроде

(defmacro ttt
  ([] (ttt 1))
  ([a] (ttt a 2))
  ([a b] (ttt a b 3))
  ([a b c] `(println ~a ~b ~c)))

и я запускаю с (ttt), он должен стать (println 1 2 3) и напечатать "1 2 3", но я получил

ArityException Wrong number of args (-1) passed to: t1$ttt clojure.lang.Compiler.macroexpand1 (Compiler.java:6473)

после некоторого расследования, я понимаю, что я должен написать

(defmacro ttt
  ([] `(ttt 1))
  ([a] `(ttt ~a 2))
  ([a b] `(ttt ~a ~b 3))
  ([a b c] `(println ~a ~b ~c)))

но почему первая версия не удалась? и args слишком странно понимать, откуда -1 происходит?

4b9b3361

Ответ 1

Макросы имеют два скрытых аргумента

Макросы имеют два скрытых аргумента &form и &env, которые предоставляют дополнительную информацию о вызовах и привязках для расширенного использования. Если у вас будет макро-вызов с другой версией самой арности, вы должны сами передать эти аргументы.

(defmacro baz
  ([] (baz &form &env 1))
  ([a] (baz &form &env a 2))
  ([a b] (baz &form &env a b 3))
  ([a b c] `(println ~a ~b ~c)))

user=> (macroexpand-1 '(baz))
(clojure.core/println 1 2 3)

user=> (baz)
1 2 3
nil

Сообщения исключений Arity вычитают скрытые аргументы из count

Причина, по которой вы получаете исключение (-1) arity, заключается в том, что компилятор вычитает эти два скрытых аргумента при генерации сообщения об ошибке для общего использования макросов. Истинным сообщением здесь для вашей первой версии ttt будет "Неверное количество аргументов (1)", потому что вы предоставили один аргумент a, но два дополнительных скрытых аргумента не были предоставлены с помощью самозапуска.

Макросы с несколькими объектами, не встречающиеся в дикой природе

На практике я предлагаю избегать макросов многозначности. Вместо этого рассмотрите вспомогательную функцию, чтобы выполнить большую часть работы от имени макроса. Действительно, это часто является хорошей практикой для других макросов.

(defn- bar
  ([] (bar 1))
  ([a] (bar a 2))
  ([a b] (bar a b 3))
  ([a b c] `(println ~a ~b ~c)))


(defmacro foo [& args] (apply bar args))

user=> (macroexpand-1 '(foo))
(clojure.core/println 1 2 3)

user=> (foo)
1 2 3
nil

Расширение макроса является рекурсивным

Ваша вторая версия ttt работает также из-за рекурсивного характера макроразложения

user=> (macroexpand-1 '(ttt))
(user/ttt 1)
user=> (macroexpand-1 *1)
(user/ttt 1 2)
user=> (macroexpand-1 *1)
(usr/ttt 1 2 3)
user=> (macroexpand-1 *1)
(clojure.core/println 1 2 3)

Итак,

user=> (macroexpand '(ttt))
(clojure.core/println 1 2 3)

Ответ 2

Когда Clojure обрабатывает определение макроса ttt, оно еще не создано и не может использоваться для преобразования исходного кода внутри макроопределения. Для компилятора ваш макрос - это что-то вроде (ну, не совсем, но это хороший пример):

(defmacro ttt0 []       (ttt1 1))
(defmacro ttt1 [a]      (ttt2 a 2))
(defmacro ttt2 [a b]    (ttt3 a b 3))
(defmacro ttt3 [a b c] `(println ~a ~b ~c))

Попробуйте оценить значение ttt0, вы получите:

CompilerException java.lang.RuntimeException: не удается разрешить символ: ttt1 в этом контексте

Итак, когда Clojure обрабатывает определение макроса, , он должен развернуть макросы в некотируемых частях определения, как и для любой другой части кода. Он терпит неудачу с ttt1 и должен завершиться неудачно в вашем случае. Я предполагаю, что это что-то вроде ошибки. Трудно сказать, почему вы получаете -1, я думаю, что это связано с внутренними механизмами реализации языка.

Здесь мы можем видеть разницу между макросами и функциями: макросы работают над любым кодом ввода, чтобы преобразовать его немедленно, в то время как нужно вызвать функцию и все всегда определено и готово для нее:

user> (defn ttt
        ([] (ttt 1))
        ([a] (ttt a 2))
        ([a b] (ttt a b 3))
        ([a b c] :works!))
;; => #'user/ttt
user> (ttt)
;; => :works!

Здесь вызовы ttt - это просто инструкции, они будут выполняться при вызове ttt.