#macros #lisp #common-lisp
#макросы #шепелявый #common-лисп
Вопрос:
Учитывая макрос:
(defclass sample-class ()
((slot-1 :accessor slot-1
:initform "sample slot")))
(defvar *sample-instance*(make-instance 'sample-class))
(defmacro sample-macro (p)
`(if (typep ,p 'sample-class)
(progn
(print "evaluated")
(print ,(slot-1 p)))))
(sample-macro *sample-instance*)
Я в замешательстве относительно того, почему это вывод ошибки
Execution of a form compiled with errors.
Form:
(SAMPLE-MACRO *SAMPLE-INSTANCE*)
Compile-time error:
(during macroexpansion of (SAMPLE-MACRO *SAMPLE-INSTANCE*))
There is no applicable method for the generic function
#<STANDARD-GENERIC-FUNCTION COMMON-LISP-USER::SLOT-1 (1)>
when called with arguments
(*SAMPLE-INSTANCE*).
See also:
The ANSI Standard, Section 7.6.6
[Condition of type SB-INT:COMPILED-PROGRAM-ERROR]
Разве макрос не должен расширяться и оценивать s-форму в процессе? Почему читатель не находит универсальную функцию slot-1
?
Ответ №1:
Я думаю, вы не понимаете, что делают макросы. Макросы — это преобразования исходного кода. Итак, рассмотрим, что происходит, когда система пытается развернуть форму макроса (sample-macro *sample-instance*)
. Во время макрорасширения p
это символ *sample-instance*
: представление фрагмента исходного кода.
Итак, теперь посмотрите на форму с обратными кавычками в теле макроса: в нем есть ,(slot-1 p)
: this попытается вызвать slot-1
все, что p
связано с, что является символом. Затем происходит сбой, и в результате происходит сбой макрорасширения.
Ну, вы могли бы «исправить» это таким образом, который кажется очевидным:
(defmacro sample-macro (p)
`(if (typep ,p 'sample-class)
(progn
(print "evaluated")
(print (slot-1 ,p)))))
И это, похоже, работает. Использование трассировщика макрорасширения:
(sample-macro *sample-instance*)
-> (if (typep *sample-instance* 'sample-class)
(progn (print "evaluated") (print (slot-1 *sample-instance*))))
И если вы используете макрос, он будет «работать». За исключением того, что он вообще не будет работать: рассмотрим эту форму: (sample-macro (make-instance 'sample-class))
: ну, давайте посмотрим на это с помощью трассировщика макросов:
(sample-macro (make-instance 'sample-class))
-> (if (typep (make-instance 'sample-class) 'sample-class)
(progn
(print "evaluated")
(print (slot-1 (make-instance 'sample-class)))))
О боже.
Таким образом, мы могли бы обойти эту проблему, переписав макрос следующим образом:
(defmacro sample-macro (p)
`(let ((it ,p))
(if (typep it 'sample-class)
(progn
(print "evaluated")
(print (slot-1 it)))
А теперь
(sample-macro (make-instance 'sample-class))
-> (let ((it (make-instance 'sample-class)))
(if (typep it 'sample-class)
(progn (print "evaluated") (print (slot-1 it)))))
Что еще лучше. И в данном случае это даже безопасно, но в подавляющем большинстве случаев нам нужно было бы использовать генсим для того, что я назвал it
:
(defmacro sample-macro (p)
(let ((itn (make-symbol "IT"))) ;not needed for this macro
`(let ((,itn ,p))
(if (typep ,itn 'sample-class)
(progn
(print "evaluated")
(print (slot-1 ,itn)))))))
А теперь:
(sample-macro (make-instance 'sample-class))
-> (let ((#:it (make-instance 'sample-class)))
(if (typep #:it 'sample-class)
(progn (print "evaluated") (print (slot-1 #:it)))))
Итак, эта (а на самом деле и предыдущая ее версия) наконец-то работает.
Но подождите, но подождите. То, что мы сделали, — это превратили эту штуку во что-то, что:
- привязывает значение своего аргумента к переменной;
- и вычисляет некоторый код с этой привязкой.
Есть название для чего-то, что это делает, и это имя — функция.
(defun not-sample-macro-any-more (it)
(if (typep it 'sample-class)
(progn
(print "evaluated")
(print (slot-1 it)))))
Это делает все, что sample-macro
делали рабочие версии, но без всякой ненужной сложности.
Ну, он не делает одну вещь: он не расширяется встроенным образом, и, возможно, это означает, что он может быть немного медленнее.
Что ж, во времена лиспа, работающего на угле, это было настоящей проблемой. Системы Lisp, работающие на угле, имели примитивные компиляторы, сделанные из древесной стружки и опилок, и работали на компьютерах, которые действительно были очень медленными. Таким образом, люди писали бы вещи, которые семантически должны быть функциями как макросы, чтобы компилятор wood-shaving встроил код. И иногда это даже стоило того.
Но теперь у нас есть продвинутые компиляторы (хотя, вероятно, все еще в основном сделанные из древесной стружки и опилок), и мы можем сказать, что мы на самом деле имеем в виду:
(declaim (inline not-sample-macro-any-more))
(defun not-sample-macro-any-more (it)
(if (typep it 'sample-class)
(progn
(print "evaluated")
(print (slot-1 it)))))
И теперь вы можете быть разумно уверены, что not-sample-macro-any-more
будете скомпилированы inline.
Еще лучше в этом случае (но ценой почти наверняка отсутствия встроенного материала):
(defgeneric not-even-slightly-sample-macro (it)
(:method (it)
(declare (ignore it))
nil))
(defmethod not-even-slightly-sample-macro ((it sample-class))
(print "evaluated")
(print (slot-1 it)))
Итак, краткое изложение здесь таково:
Используйте макросы для того, для чего они предназначены, то есть для преобразования исходного кода. Если вы не хотите этого делать, используйте функции. Если вы уверены, что процесс вызова функций занимает много времени, то подумайте о том, чтобы объявить их встроенными, чтобы избежать этого.
Комментарии:
1. Спасибо, что нашли время написать это. Это было очень познавательно и подробно.
2. «В системах Lisp, работающих на угле, были примитивные компиляторы, сделанные из древесной стружки и опилок …» ХА! Мне это нравится!
Ответ №2:
В других ответах объяснялось, что выполнение макроса связано с преобразованием исходного кода и значений, доступных во время расширения макроса.
Давайте также попробуем разобраться в сообщении об ошибке. Мы должны понимать это буквально:
Execution of a form compiled with errors.
Выше говорится, что речь идет о компиляции.
Form:
(SAMPLE-MACRO *SAMPLE-INSTANCE*)
Выше приведена исходная форма, которая должна быть скомпилирована.
Compile-time error:
(during macroexpansion of (SAMPLE-MACRO *SAMPLE-INSTANCE*))
Снова: компиляция и теперь конкретно во время расширения макроса.
There is no applicable method for the generic function
#<STANDARD-GENERIC-FUNCTION COMMON-LISP-USER::SLOT-1 (1)>
when called with arguments
(*SAMPLE-INSTANCE*).
Теперь выше интересная часть: не существует применимого метода для универсальной функции SLOT-1
и аргумента *SAMPLE-INSTANCE*
.
Что такое *SAMPLE-INSTANCE*
? Это символ. В вашем коде есть метод, но он предназначен для экземпляров класса sample-class
. Но для символов нет метода. Так что это не сработает:
(setf p '*sample-instance*)
(slot-1 p)
Это в основном то, что сделал ваш код. Вы ожидали работать со значениями во время выполнения, но все, что вы получили во время компиляции, — это исходный символ…
Сообщение об ошибке компилятора, показывающее ошибку времени компиляции с элементом исходного кода, указывает на то, что происходит путаница в вычислениях времени выполнения и времени расширения макроса.
See also:
The ANSI Standard, Section 7.6.6
[Condition of type SB-INT:COMPILED-PROGRAM-ERROR]
Комментарии:
1. Спасибо, что ответили. Это имеет некоторый смысл, но я все еще немного смущен ошибкой. В моем понимании код после запятой должен оцениваться так, как если бы это происходило во время выполнения. Почему это по-другому? Я буду продолжать изучать его, так как я уверен, что есть что-то, что я неправильно понимаю
2. @cstml нет, код после запятой вычисляется во время развертывания макроса. Это шаблон обратной кавычки, и макрос возвращает развернутый шаблон во время развертывания макроса. Затем возвращенный код компилируется и позже выполняется во время выполнения.
3. Я понимаю. Хорошо, это очень помогает на самом деле
Ответ №3:
Чтобы понять, что делает макрос, давайте использовать macroexpand
.
(macroexpand-1 '(sample-macro *sample-instance*))
=>
There is no applicable method for the generic function
#<STANDARD-GENERIC-FUNCTION COMMON-LISP-USER::SLOT-1 (1)>
when called with arguments
(*SAMPLE-INSTANCE*).
[Condition of type SB-PCL::NO-APPLICABLE-METHOD-ERROR]
Упс, то же сообщение об ошибке. Я упростю макрос и удалю оценку вокруг slot-1
.
(defmacro sample-macro (p)
`(if (typep ,p 'sample-class)
(progn
(print "evaluated")
(print (slot-1 p)))))
(macroexpand-1 '(sample-macro *sample-instance*))
=>
(IF (TYPEP *SAMPLE-INSTANCE* 'SAMPLE-CLASS)
(PROGN (PRINT "evaluated") (PRINT (SLOT-1 P))))
Код выглядит хорошо до тех пор , пока не появится переменная P
. Так будет ли это работать с, просто, ,p
? Нет необходимости писать ,(slot-1 p)
, так slot-1
как здесь все правильно.
(defmacro sample-macro (p)
`(if (typep ,p 'sample-class)
(progn
(print "evaluated")
(print (slot-1 ,p)))))
(macroexpand-1 '(sample-macro *sample-instance*))
=>
(IF (TYPEP *SAMPLE-INSTANCE* 'SAMPLE-CLASS)
(PROGN (PRINT "evaluated") (PRINT (SLOT-1 *SAMPLE-INSTANCE*))))
Код выглядит корректно.
(sample-macro *sample-instance*)
"evaluated"
"sample slot"
и это работает.
Комментарии:
1. Спасибо вам за ответ. Я пытался вставить туда строку во время выполнения. Вот почему я был сбит с толку этим.