Ответ 1
Вы смотрите на это:
(defmacro once-only ((&rest names) &body body)
(let ((gensyms (loop for n in names collect (gensym))))
`(let (,@(loop for g in gensyms collect `(,g (gensym))))
`(let (,,@(loop for g in gensyms for n in names collect ``(,,g ,,n)))
,(let (,@(loop for n in names for g in gensyms collect `(,n ,g)))
,@body)))))
Это не так сложно, но у него есть вложенный backquote и несколько уровней, которые похожи друг на друга, что приводит к легкой путанице даже для опытных кодеров Lisp.
Это макрос, который используется макросами для записи их расширений: макрос, который записывает части тел макросов.
В теле самого макроса есть простая let
, затем генерируется один раз назад созданный let
, который будет жить внутри тела макроса, который используется once-only
. Наконец, в макрокоманде макроса , который, на кодовом сайте, где используется макрос, пользователь получает двойной обратный отсчет let
.
Два раунда генерации gensyms необходимы, потому что once-only
является самим макросом, поэтому он должен быть гигиеничным для самого себя; поэтому он генерирует кучу gensyms для себя в самом дальнем let
. Но также целью once-only
является упрощение написания другого гигиенического макроса. Таким образом, он также генерирует gensym для этого макроса.
В двух словах, once-only
необходимо создать макроразложение, которое требует некоторых локальных переменных, значения которых являются gensyms. Эти локальные переменные будут использоваться для вставки gensyms в другое расширение макроса, чтобы сделать его гигиеничным. И эти локальные переменные сами по себе должны быть гигиеничными, поскольку они являются макроразложениями, поэтому они также являются gensyms.
Если вы пишете простой макрос, у вас есть локальные переменные, которые содержат gensyms, например:
;; silly example
(defmacro repeat-times (count-form &body forms)
(let ((counter-sym (gensym)))
`(loop for ,counter-sym below ,count-form do ,@forms)))
В процессе написания макроса вы создали символ counter-sym
. Эта переменная определяется в простом виде. Вы, человек, выбрали его таким образом, чтобы он не столкнулся ни с чем в лексической сфере. Лексическая область, о которой идет речь, относится к вашему макросу. Нам не нужно беспокоиться о counter-sym
случайном захвате ссылок внутри count-form
или forms
, потому что forms
- это просто данные, которые входят в фрагмент кода, который будет вставлен в какую-то удаленную лексическую область (сайт где используется макрос). Нам нужно беспокоиться о том, чтобы не путать counter-sym
с другой переменной внутри нашего макроса. Например, мы не можем дать нашей локальной переменной имя count-form
. Зачем? Поскольку это имя является одним из наших аргументов функции; мы будем затенять его, создавая ошибку программирования.
Теперь, если вы хотите, чтобы макрос помог вам написать этот макрос, машина должна выполнить ту же работу, что и вы. Когда он пишет код, он должен изобрести имя переменной, и он должен быть осторожным в отношении того, какое имя он изобретает.
Однако, машина написания кода, в отличие от вас, не видит окружающую область. Он не может просто посмотреть, какие переменные есть и выбрать те, которые не сталкиваются. Машина - это просто функция, которая принимает некоторые аргументы (фрагменты необоснованного кода) и создает фрагмент кода, который затем слепо заменяется в область после того, как машина выполнила свою работу.
Следовательно, машина должна выбирать имена с умом. На самом деле, чтобы быть полностью пуленепробиваемым, он должен быть параноидальным и использовать символы, которые являются совершенно уникальными: gensyms.
Итак, продолжая этот пример, предположим, что у нас есть робот, который напишет это тело макроса для нас. Этот робот может быть макросом, repeat-times-writing-robot
:
(defmacro repeat-times (count-form &body forms)
(repeat-times-writing-robot count-form forms)) ;; macro call
Как выглядит макрос робота?
(defmacro repeat-times-writing-robot (count-form forms)
(let ((counter-sym-sym (gensym))) ;; robot gensym
`(let ((,counter-sym-sym (gensym))) ;; the ultimate gensym for the loop
`(loop for ,,counter-sym-sym below ,,count-form do ,@,forms))))
Вы можете видеть, как это имеет некоторые особенности once-only
: двойное вложение и два уровня (gensym)
. Если вы это понимаете, то прыжок на once-only
мал.
Конечно, если бы мы просто хотели, чтобы робот писал повторяющиеся времена, мы бы сделали его функцией, и тогда этой функции не пришлось бы беспокоиться о том, чтобы изобретать переменные: это не макрос, и поэтому он не нужна гигиена:
;; i.e. regular code refactoring: a piece of code is moved into a helper function
(defun repeat-times-writing-robot (count-form forms)
(let ((counter-sym (gensym)))
`(loop for ,counter-sym below ,count-form do ,@forms)))
;; ... and then called:
(defmacro repeat-times (count-form &body forms)
(repeat-times-writing-robot count-form forms)) ;; just a function now
Но once-only
не может быть функцией, потому что ее задание заключается в том, чтобы изобретать переменные от имени своего босса, макроса, который его использует, а функция не может вводить переменные в вызывающего абонента.