Вы смотрите на это:
(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)))))
Это не так сложно, но у него есть вложенная обратная цитата и несколько уровней, которые похожи друг на друга, что приводит к легкому замешательству даже для опытных кодировщиков Lisp.
Это макрос, который используется макросами для написания своих расширений: макрос, который записывает части тел макросов.
В теле самого макроса есть простой let
, затем сгенерированный с обратной кавычкой let
, который будет жить внутри тела макроса, использующего once-only
. Наконец, есть двойная обратная кавычка let
, которая появится в раскрытии макроса макроса , который , на сайте кода, где макрос используется пользователем.
Необходимы два цикла генерации гензимов, потому что once-only
сам по себе макрос, и поэтому он должен быть гигиеничным ради самого себя; таким образом, он генерирует кучу гензимов для самого себя let
. Но также цель once-only
- упростить написание другого гигиенического макроса. Таким образом, он генерирует gensyms и для этого макроса.
В двух словах, once-only
необходимо создать макроразложение, для которого требуются некоторые локальные переменные, значения которых являются генсимами. Эти локальные переменные будут использоваться для вставки гензимов в другое расширение макроса, чтобы сделать его гигиеническим. И эти локальные переменные сами по себе должны быть гигиеническими, поскольку они являются макроразложением, поэтому они также являются 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's 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
не может быть функцией, потому что ее работа состоит в том, чтобы изобретать переменные от имени своего босса, макроса, который его использует, и функция не может вводить переменные в свой вызывающий абонент.