Понимание того, как реализовать одноразовый lisp макрос - программирование
Подтвердить что ты не робот

Понимание того, как реализовать одноразовый lisp макрос

В книге Питера Зайбеля "Практическое общее Lisp" мы можем найти определение очень сложного макроса один раз (см. нижнюю часть страницы http://www.gigamonkeys.com/book/macros-defining-your-own.html).

Я читаю это определение макроса 10 раз за последние 3 недели и не могу понять, как он работает.:( Хуже того, я не могу самостоятельно разработать этот макрос, хотя я понимаю его цель и как его использовать.

Меня особенно интересует систематический "вывод" этого пресловутого жесткого макроса, шаг за шагом! Любая помощь?

4b9b3361

Ответ 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 не может быть функцией, потому что ее задание заключается в том, чтобы изобретать переменные от имени своего босса, макроса, который его использует, а функция не может вводить переменные в вызывающего абонента.

Ответ 2

Альтернатива макросу once-only из Practical Common Lisp выводится в Let Over Lambda (см. раздел "Только один" в третьей главе).

Ответ 3

Каз объяснил это красиво и широко.

Однако, если вы не заботитесь о проблеме двойной гигиены, вам может быть легче понять это:

(defmacro once-only ((&rest symbols) &body body)
  ;; copy-symbol may reuse the original symbol name
  (let ((uninterned-symbols (mapcar 'copy-symbol symbols)))
    ;; For the final macro expansion:
    ;; Evaluate the forms in the original bound symbols into fresh bindings
    ``(let (,,@(mapcar #'(lambda (uninterned-symbol symbol)
                           ``(,',uninterned-symbol ,,symbol))
                       uninterned-symbols symbols))
        ;; For the macro that is using us:
        ;; Bind the original symbols to the fresh symbols
        ,(let (,@(mapcar #'(lambda (symbol uninterned-symbol)
                             `(,symbol ',uninterned-symbol))
                         symbols uninterned-symbols))
           ,@body))))

Первый let дважды зацикливается, потому что он будет частью окончательного расширения. Цель состоит в том, чтобы оценить формы в исходных связанных символах в новые привязки.

Второй let будет возвращен один раз, потому что он будет частью пользователя once-only. Цель состоит в том, чтобы восстановить исходные символы до свежих символов, поскольку их формы будут оценены и привязаны к ним в конечном расширении.

Если восстановление первоначальных символов было до окончательного расширения макроса, окончательное расширение макроса будет ссылаться на неинтерминированные символы вместо исходных форм.

Реализация with-slots, которая использует once-only, является примером, который требует двойной гигиены:

(defmacro with-slots ((&rest slots) obj &body body)
  (once-only (obj)
    `(symbol-macrolet (,@(mapcar #'(lambda (slot)
                                     `(,slot (slot-value ,obj ',slot)))
                                 slots))
       ,@body)))

;;; Interaction in a REPL    
> (let ((*gensym-counter* 1)
        (*print-circle* t)
        (*print-level* 10))
    (pprint (macroexpand `(with-slots (a) (make-object-1)
                            ,(macroexpand `(with-slots (b) (make-object-2)
                                             body))))))

;;; With the double-hygienic once-only
(let ((#1=#:g2 (make-object-1)))
  (symbol-macrolet ((a (slot-value #1# 'a)))
    (let ((#2=#:g1 (make-object-2)))
      (symbol-macrolet ((b (slot-value #2# 'b)))
        body))))

;;; With this version of once-only
(let ((#1=#:obj (make-object-1)))
  (symbol-macrolet ((a (slot-value #1# 'a)))
    (let ((#1# (make-object-2)))
      (symbol-macrolet ((b (slot-value #1# 'b)))
        body))))

Второе расширение показывает, что внутренний let затеняет привязку к переменной #:obj outter let. Таким образом, доступ к a внутри внутреннего with-slots будет фактически иметь доступ ко второму объекту.

Обратите внимание, что в этом примере макро-расширение outter получает gensym с именем g2 и внутренним g1. При обычной оценке или компиляции это было бы наоборот, так как формы шли от внешнего к внутреннему.