common-lisp
макрос
Поиск…
замечания
Цель макросов
Макросы предназначены для генерации кода, преобразования кода и предоставления новых обозначений. Эти новые обозначения могут быть более подходящими для лучшего выражения программы, например, путем создания конструкций на уровне домена или целых новых внедренных языков.
Макросы могут сделать исходный код более понятным, но отладка может быть затруднена. Как правило, нельзя использовать макросы, когда будет выполняться регулярная функция. Когда вы их используете, избегайте обычных ловушек, старайтесь придерживаться обычно используемых шаблонов и соглашений об именах.
Заказ Macroexpansion
По сравнению с функциями макросы расширяются в обратном порядке; крайний крайний, самый последний. Это означает, что по умолчанию нельзя использовать внутренний макрос для генерации синтаксиса, необходимого для внешнего макроса.
Порядок оценки
Иногда макросы должны перемещать пользовательские формы. Нужно следить за тем, чтобы не изменять порядок их оценки. Пользователь может полагаться на побочные эффекты, происходящие по порядку.
Оценить только один раз
Расширение макроса часто требует использования значения одной и той же пользовательской формы более одного раза. Возможно, что форма имеет побочные эффекты или может вызвать дорогостоящую функцию. Таким образом, макрос должен обязательно оценивать только такие формы один раз. Обычно это делается путем назначения значения локальной переменной (имя которой GENSYM
ed).
Функции, используемые макросами, используя EVAL-WHEN
Сложные макросы часто имеют части своей логики, реализованные в отдельных функциях. Однако следует помнить, что макросы расширяются до того, как будет скомпилирован фактический код. При компиляции файла по умолчанию функции и переменные, определенные в одном файле, не будут доступны во время выполнения макроса. Все определения функций и переменных в том же файле, которые используются макросом, должны быть обернуты внутри формы EVAL-WHEN
. EVAL-WHEN
должно быть указано все три раза, когда прилагаемый код также должен быть оценен во время загрузки и времени выполнения.
(eval-when (:compile-toplevel :load-toplevel :execute)
(defun foobar () ...))
Это не относится к функциям, вызванным расширением макроса, только те, которые вызываются самим макросом.
Общие шаблоны макросов
TODO: Возможно, переместите пояснения к замечаниям и добавьте примеры отдельно
FOOF
В Common Lisp существует концепция обобщенных ссылок . Они позволяют программисту устанавливать значения в разные «места», как если бы они были переменными. Макросы, которые используют эту способность, часто имеют имя F
-postfix. Обычно это первый аргумент макроса.
Примеры из стандарта: INCF
, DECF
, ROTATEF
, SHIFTF
, REMF
.
Глупый пример, макрос, который переворачивает знак магазина чисел в месте:
(defmacro flipf (place)
`(setf ,place (- ,place)))
С-FOO
Макросы, которые приобретают и безопасно выпускают ресурс, обычно называются WITH-
-prefix. Обычно макрос должен использовать синтаксис, например:
(with-foo (variable details-of-the-foo...)
body...)
Примеры из стандарта: WITH-OPEN-FILE
, WITH-OPEN-STREAM
, WITH-INPUT-FROM-STRING
, WITH-OUTPUT-TO-STRING
.
Один из подходов к реализации этого типа макросов, который может избежать некоторых ошибок в загрязнении имен и непреднамеренной множественной оценки, заключается в том, что сначала внедряется функциональная версия. Например, первый шаг в реализации макроса with-widget
который безопасно создает виджет и очищает после него, может быть функцией:
(defun call-with-widget (args function)
(let ((widget (apply #'make-widget args))) ; obtain WIDGET
(unwind-protect (funcall function widget) ; call FUNCTION with WIDGET
(cleanup widget) ; cleanup
Поскольку это функция, нет никаких проблем в отношении объема имен внутри функции или поставщика , и это упрощает запись соответствующего макроса:
(defmacro with-widget ((var &rest args) &body body)
`(call-with-widget (list ,@args) (lambda (,var) ,@body)))
DO-FOO
Макросы, которые перебирают что-то, часто называются DO
-prefix. Макро синтаксис обычно должен быть в форме
(do-foo (variable the-foo-being-done return-value)
body...)
Примеры из стандарта: DOTIMES
, DOLIST
, DO-SYMBOLS
.
ЛОКАЗА, ЭФОКАЗА, CFOOCASE
Макросы, соответствующие входным данным в некоторых случаях, часто называются CASE
-postfix. Часто существует E...CASE
-вариант, который сигнализирует об ошибке, если вход не соответствует ни одному из случаев, и C...CASE
, который сигнализирует о продолжающейся ошибке. Они должны иметь такой синтаксис, как
(foocase input
(case-to-match-against (optionally-some-params-for-the-case)
case-body-forms...)
more-cases...
[(otherwise otherwise-body)])
Примеры из стандарта: CASE
, TYPECASE
, TYPECASE
HANDLER-CASE
.
Например, макрос, который сопоставляет строку с регулярными выражениями и связывает группы регистров с переменными. Использует CL-PPCRE для регулярных выражений.
(defmacro regexcase (input &body cases)
(let ((block-sym (gensym "block"))
(input-sym (gensym "input")))
`(let ((,input-sym ,input))
(block ,block-sym
,@(loop for (regex vars . body) in cases
if (eql regex 'otherwise)
collect `(return-from ,block-sym (progn ,vars ,@body))
else
collect `(cl-ppcre:register-groups-bind ,vars
(,regex ,input-sym)
(return-from ,block-sym
(progn ,@body))))))))
(defun test (input)
(regexcase input
("(\\d+)-(\\d+)" (foo bar)
(format t "Foo: ~a, Bar: ~a~%" foo bar))
("Foo: (\\w+)$" (foo)
(format t "Foo: ~a.~%" foo))
(otherwise (format t "Didn't match.~%"))))
(test "asd 23-234 qwe")
; Foo: 23, Bar: 234
(test "Foo: Foobar")
; Foo: Foobar.
(test "Foo: 43 - 23")
; Didn't match.
DEFINE-FOO, DEFFOO
Макросы, которые определяют вещи, обычно называются либо DEFINE-
либо DEF
-prefix.
Примеры из стандарта: DEFUN
, DEFMACRO
, DEFINE-CONDITION
.
Анафорические макросы
Anaphoric Macro - это макрос, который вводит переменную (часто IT
), которая захватывает результат предоставленной пользователем формы. Общим примером является Anaphhoric If, который похож на обычный IF
, но также определяет переменную IT
для ссылки на результат тестовой формы.
(defmacro aif (test-form then-form &optional else-form)
`(let ((it ,test-form))
(if it ,then-form ,else-form)))
(defun test (property plist)
(aif (getf plist property)
(format t "The value of ~s is ~a.~%" property it)
(format t "~s wasn't in ~s!~%" property plist)))
(test :a '(:a 10 :b 20 :c 30))
; The value of :A is 10.
(test :d '(:a 10 :b 20 :c 30))
; :D wasn't in (:A 10 :B 20 :C 30)!
MACROEXPAND
Расширение макросов - это процесс превращения макросов в фактический код. Обычно это происходит как часть процесса компиляции. Компилятор будет расширять все макроформы до фактического компиляции кода. Макро расширение также происходит во время интерпретации кода Lisp.
Можно вручную вызвать MACROEXPAND
чтобы узнать, к чему расширяется MACROEXPAND
.
CL-USER> (macroexpand '(with-open-file (file "foo")
(do-something-with file)))
(LET ((FILE (OPEN "foo")) (#:G725 T))
(UNWIND-PROTECT
(MULTIPLE-VALUE-PROG1 (PROGN (DO-SOMETHING-WITH FILE)) (SETQ #:G725 NIL))
(WHEN FILE (CLOSE FILE :ABORT #:G725))))
MACROEXPAND-1
тот же, но только один раз расширяется. Это полезно при попытке понять макроскопическую форму, которая расширяется до другой макроформы.
CL-USER> (macroexpand-1 '(with-open-file (file "foo")
(do-something-with file)))
(WITH-OPEN-STREAM (FILE (OPEN "foo")) (DO-SOMETHING-WITH FILE))
Обратите внимание, что ни MACROEXPAND
ни MACROEXPAND-1
расширяют код Lisp на всех уровнях. Они только расширяют макрос верхнего уровня. Для макроэкспонирования полной формы на всех уровнях для этого нужен ходок кода . Это средство не предусмотрено стандартом Common Lisp.
Backquote - создание шаблонов кода для макросов
Код возврата макросов. Поскольку код в Lisp состоит из списков, для его создания можно использовать обычные функции управления списком.
;; A pointless macro
(defmacro echo (form)
(list 'progn
(list 'format t "Form: ~a~%" (list 'quote form))
form))
Это часто очень трудно читать, особенно в более длинных макросах. Макрос чтения Backquote позволяет писать шаблоны с кавычками , которые заполняются выборочной оценкой элементов.
(defmacro echo (form)
`(progn
(format t "Form: ~a~%" ',form)
,form))
(macroexpand '(echo (+ 3 4)))
;=> (PROGN (FORMAT T "Form: ~a~%" '(+ 3 4)) (+ 3 4))
Эта версия выглядит почти как обычный код. Запятые используются для оценки FORM
; все остальное возвращается как есть. Обратите внимание, что в ',form
одинарной кавычки находится за запятой, поэтому она будет возвращена.
Можно также использовать ,@
для сращивания списка в позиции.
(defmacro echo (&rest forms)
`(progn
,@(loop for form in forms collect `(format t "Form: ~a~%" ,form))
,@forms))
(macroexpand '(echo (+ 3 4)
(print "foo")
(random 10)))
;=> (PROGN
; (FORMAT T "Form: ~a~%" (+ 3 4))
; (FORMAT T "Form: ~a~%" (PRINT "foo"))
; (FORMAT T "Form: ~a~%" (RANDOM 10))
; (+ 3 4)
; (PRINT "foo")
; (RANDOM 10))
Backquote можно использовать и вне макросов.
Уникальные символы для предотвращения конфликтов имен в макросах
Расширение макроса часто требует использования символов, которые не были переданы в качестве аргументов пользователем (например, имена локальных переменных). Нужно убедиться, что такие символы не могут конфликтовать с символом, который пользователь использует в окружающем коде.
Обычно это достигается с помощью функции GENSYM
, которая возвращает новый символ без символа.
Плохой
Рассмотрим макрос ниже. Он делает DOTIMES
-loop, который также собирает результат тела в список, который возвращается в конце.
(defmacro dotimes+collect ((var count) &body body)
`(let ((result (list)))
(dotimes (,var ,count (nreverse result))
(push (progn ,@body) result))))
(dotimes+collect (i 5)
(format t "~a~%" i)
(* i i))
; 0
; 1
; 2
; 3
; 4
;=> (0 1 4 9 16)
Кажется, что это работает в этом случае, но если у пользователя произошло переменное имя RESULT
, которое они используют в теле, результаты, вероятно, не будут тем, что ожидает пользователь. Рассмотрим эту попытку написать функцию, которая собирает список сумм всех целых чисел до N
:
(defun sums-upto (n)
(let ((result 0))
(dotimes+collect (i n)
(incf result i))))
(sums-upto 10) ;=> Error!
Хорошо
Чтобы решить эту проблему, мы должны использовать GENSYM
для создания уникального имени для RESULT
-переменного в макроподстановках.
(defmacro dotimes+collect ((var count) &body body)
(let ((result-symbol (gensym "RESULT")))
`(let ((,result-symbol (list)))
(dotimes (,var ,count (nreverse ,result-symbol))
(push (progn ,@body) ,result-symbol)))))
(sums-upto 10) ;=> (0 1 3 6 10 15 21 28 36 45)
TODO: Как сделать символы из строк
TODO: устранение проблем с символами в разных пакетах
если-let, когда-let, -let-макросы
Эти макросы объединяют поток управления и связывание. Это улучшение по сравнению с анафорическими анафорическими макросами, потому что они позволяют разработчику передавать смысл посредством именования. Поэтому их использование рекомендуется по сравнению с анафорическими аналогами.
(if-let (user (get-user user-id))
(show-dashboard user)
(redirect 'login-page))
FOO-LET
связывают одну или несколько переменных, а затем используют эти переменные в качестве тестовой формы для соответствующего условного ( IF
, WHEN
). Множественные переменные объединяются с AND
. Выбранная ветвь выполняется с действующими связями. Простая реализация переменной IF-LET
одной переменной может выглядеть примерно так:
(defmacro if-let ((var test-form) then-form &optional else-form)
`(let ((,var ,test-form))
(if ,var ,then-form ,else-form)))
(macroexpand '(if-let (a (getf '(:a 10 :b 20 :c 30) :a))
(format t "A: ~a~%" a)
(format t "Not found.~%")))
; (LET ((A (GETF '(:A 10 :B 20 :C 30) :A)))
; (IF A
; (FORMAT T "A: ~a~%" A)
; (FORMAT T "Not found.~%")))
Версия, поддерживающая несколько переменных, доступна в библиотеке Александрии .
Использование макросов для определения структур данных
Общее использование макросов заключается в создании шаблонов для структур данных, которые подчиняются общим правилам, но могут содержать разные поля. Записывая макрос, вы можете разрешить детальное конфигурирование структуры данных без необходимости повторять шаблонный код и не использовать менее эффективную структуру (например, хеш) в памяти, чтобы упростить программирование.
Например, предположим, что мы хотим определить несколько классов, которые имеют ряд разных свойств, каждый из которых имеет геттер и сеттер. Кроме того, для некоторых (но не всех) этих свойств мы хотим, чтобы setter вызывал метод на объекте, уведомляющем его о том, что свойство было изменено. Хотя Common LISP уже имеет сокращение для написания геттеров и сеттеров, написание стандартного настраиваемого сеттера таким образом обычно требует дублирования кода, который вызывает метод уведомления в каждом сетевом устройстве, что может быть больно, если имеется большое количество свойств , Однако, определяя макрос, это становится намного проще:
(defmacro notifier (class slot)
"Defines a setf method in (class) for (slot) which calls the object's changed method."
`(defmethod (setf ,slot) (val (item ,class))
(setf (slot-value item ',slot) val)
(changed item ',slot)))
(defmacro notifiers (class slots)
"Defines setf methods in (class) for all of (slots) which call the object's changed method."
`(progn
,@(loop for s in slots collecting `(notifier ,class ,s))))
(defmacro defclass-notifier-slots (class nslots slots)
"Defines a class with (nslots) giving a list of slots created with notifiers, and (slots) giving a list of slots created with regular accessors."
`(progn
(defclass ,class ()
( ,@(loop for s in nslots collecting `(,s :reader ,s))
,@(loop for s in slots collecting `(,s :accessor ,s))))
(notifiers ,class ,nslots)))
Теперь мы можем написать (defclass-notifier-slots foo (bar baz qux) (waldo))
и сразу определить класс foo
с обычным слотом waldo
(созданным второй частью макроса со спецификацией (waldo :accessor waldo)
) , и слоты bar
, baz
и qux
с сеттерами, которые вызывают changed
метод (где геттер определяется первой частью макроса (bar :reader bar)
и сеттер с помощью макроса вызываемого notifier
).
В дополнение к тому, что мы можем быстро определить несколько классов, которые ведут себя таким образом, с большим количеством свойств без повторения, мы получаем обычное преимущество повторного использования кода: если позже мы решим изменить способ работы методов уведомления, мы можем просто изменить макрос, и структура каждого класса, использующего его, изменится.