Понимание того, как реализовать однократный макрос lisp - PullRequest
16 голосов
/ 21 марта 2012

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

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

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

Ответы [ 3 ]

25 голосов
/ 21 марта 2012

Вы смотрите на это:

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

7 голосов
/ 21 марта 2012

Альтернатива макросу once-only из Practical Common Lisp получена в Let Over Lambda (см. Раздел «Один раз» в третьей главе).

2 голосов
/ 15 апреля 2012

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

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

(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 внешнего let.Таким образом, доступ к a внутри внутреннего with-slots фактически приведет к доступу ко второму объекту.

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

...