common-lisp
macros
Buscar..
Observaciones
El propósito de las macros
Las macros están destinadas a generar código, transformar código y proporcionar nuevas notaciones. Estas nuevas notaciones pueden ser más adecuadas para expresar mejor el programa, por ejemplo, proporcionando construcciones a nivel de dominio o nuevos lenguajes incorporados.
Las macros pueden hacer que el código fuente sea más autoexplicativo, pero la depuración puede ser más difícil. Como regla general, uno no debe usar macros cuando una función normal funcionará. Cuando los use, evite los escollos habituales, trate de atenerse a los patrones de uso común y las convenciones de nombres.
Orden de Macroexpansion
En comparación con las funciones, las macros se expanden en orden inverso; Primero lo primero, lo último lo último. Esto significa que, de forma predeterminada, no se puede usar una macro interna para generar la sintaxis necesaria para una macro externa.
Orden de evaluación
A veces las macros necesitan mover los formularios proporcionados por el usuario. Uno debe asegurarse de no cambiar el orden en que se evalúan. El usuario puede estar confiando en los efectos secundarios que suceden en orden.
Evaluar una sola vez
La expansión de una macro a menudo necesita usar el valor del mismo formulario proporcionado por el usuario más de una vez. Es posible que la forma tenga efectos secundarios o que esté llamando a una función costosa. Por lo tanto, la macro debe asegurarse de evaluar solo tales formas una vez. Por lo general, esto se hará asignando el valor a una variable local (cuyo nombre es GENSYM
ed).
Funciones utilizadas por las macros, utilizando EVAL-WHEN
Las macros complejas a menudo tienen partes de su lógica implementada en funciones separadas. Sin embargo, hay que recordar que las macros se expanden antes de compilar el código real. Al compilar un archivo, entonces, por defecto, las funciones y variables definidas en el mismo archivo no estarán disponibles durante la ejecución de la macro. Todas las definiciones de funciones y variables, en el mismo archivo, utilizadas por una macro deben estar envueltas dentro de una forma EVAL-WHEN
. EVAL-WHEN
debe tener las tres veces especificadas, cuando el código adjunto también debe evaluarse durante la carga y el tiempo de ejecución.
(eval-when (:compile-toplevel :load-toplevel :execute)
(defun foobar () ...))
Esto no se aplica a las funciones llamadas desde la expansión de la macro, solo a las llamadas por la macro misma.
Patrones de macro comunes
TODO: Tal vez mover las explicaciones a los comentarios y agregar ejemplos por separado
FOOF
En Common Lisp, hay un concepto de referencias generalizadas . Permiten a un programador establecer valores en varios "lugares" como si fueran variables. Las macros que hacen uso de esta habilidad a menudo tienen un prefijo F
en el nombre. El lugar suele ser el primer argumento de la macro.
Ejemplos de la norma: INCF
, DECF
, ROTATEF
, SHIFTF
, REMF
.
Un ejemplo tonto, una macro que voltea el signo de un almacén de números en un lugar:
(defmacro flipf (place)
`(setf ,place (- ,place)))
CON FOO
Macros que bloquean y liberan de forma segura un recurso generalmente se nombran con un WITH-
-prefix. La macro usualmente debe usar una sintaxis como:
(with-foo (variable details-of-the-foo...)
body...)
Ejemplos de la norma: WITH-OPEN-FILE
, WITH-OPEN-STREAM
, WITH-INPUT-FROM-STRING
, WITH-OUTPUT-TO-STRING
.
Un enfoque para implementar este tipo de macro que puede evitar algunos de los inconvenientes de la contaminación de nombres y la evaluación múltiple no deseada es implementar primero una versión funcional. Por ejemplo, el primer paso en la implementación de una macro with-widget
que crea de forma segura un widget y luego se limpia puede ser una función:
(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
Debido a que esta es una función, no hay preocupaciones sobre el alcance de los nombres dentro de la función o el proveedor , y facilita la escritura de la macro correspondiente:
(defmacro with-widget ((var &rest args) &body body)
`(call-with-widget (list ,@args) (lambda (,var) ,@body)))
DO-FOO
Las macros que se repiten sobre algo a menudo se nombran con un prefijo DO
. La sintaxis de las macros debería estar normalmente en forma.
(do-foo (variable the-foo-being-done return-value)
body...)
Ejemplos de la norma: DOTIMES
, DOLIST
, DO-SYMBOLS
.
FOOCASE, EFOOCASE, CFOOCASE
Las macros que coinciden con una entrada en ciertos casos a menudo se denominan con un CASE
-postfix. A menudo hay una variante E...CASE
, que señala un error si la entrada no coincide con ninguno de los casos, y C...CASE
, que señala un error continuo. Deben tener sintaxis como
(foocase input
(case-to-match-against (optionally-some-params-for-the-case)
case-body-forms...)
more-cases...
[(otherwise otherwise-body)])
Ejemplos de la norma: CASE
, TYPECASE
, HANDLER-CASE
.
Por ejemplo, una macro que hace coincidir una cadena con expresiones regulares y vincula los grupos de registro a las variables. Utiliza CL-PPCRE para expresiones regulares.
(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
Las macros que definen cosas generalmente se nombran con DEFINE-
o DEF
-prefix.
Ejemplos de la norma: DEFUN
, DEFMACRO
, DEFINE-CONDITION
.
Macros anafóricas
Una macro anafórica es una macro que introduce una variable (a menudo IT
) que captura el resultado de un formulario proporcionado por el usuario. Un ejemplo común es el If anafórico, que es como un IF
regular, pero también define la variable IT
para referirse al resultado del formulario de prueba.
(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
La expansión de macros es el proceso de convertir macros en código real. Esto suele suceder como parte del proceso de compilación. El compilador expandirá todas las formas de macro antes de compilar realmente el código. La expansión de macros también ocurre durante la interpretación del código Lisp.
Uno puede llamar a MACROEXPAND
manualmente para ver a qué se expande una forma macro.
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
es el mismo, pero solo se expande una vez. Esto es útil cuando se trata de dar sentido a una forma macro que se expande a otra forma macro.
CL-USER> (macroexpand-1 '(with-open-file (file "foo")
(do-something-with file)))
(WITH-OPEN-STREAM (FILE (OPEN "foo")) (DO-SOMETHING-WITH FILE))
Tenga en cuenta que ni MACROEXPAND
ni MACROEXPAND-1
expanden el código Lisp en todos los niveles. Sólo expanden la forma de macro de nivel superior. Para expandir una forma completamente en todos los niveles, se necesita un caminante de código para hacerlo. Esta instalación no se proporciona en el estándar Common Lisp.
Backquote - escribiendo plantillas de código para macros
Código de retorno de macros. Dado que el código en Lisp consta de listas, se pueden usar las funciones de manipulación de listas normales para generarlas.
;; A pointless macro
(defmacro echo (form)
(list 'progn
(list 'format t "Form: ~a~%" (list 'quote form))
form))
Esto suele ser muy difícil de leer, especialmente en macros más largas. La macro del lector de Backquote permite escribir plantillas entre comillas que se completan mediante la evaluación selectiva de los elementos.
(defmacro echo (form)
`(progn
(format t "Form: ~a~%" ',form)
,form))
(macroexpand '(echo (+ 3 4)))
;=> (PROGN (FORMAT T "Form: ~a~%" '(+ 3 4)) (+ 3 4))
Esta versión se parece casi a un código regular. Las comas se utilizan para evaluar FORM
; Todo lo demás se devuelve como es. Tenga en cuenta que en ',form
la comilla simple está fuera de la coma, por lo que se devolverá.
También se puede usar ,@
para empalmar una lista en la posición.
(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 puede ser usado fuera de macros también.
Símbolos únicos para evitar choques de nombre en macros
La expansión de una macro a menudo necesita usar símbolos que no fueron pasados como argumentos por el usuario (como nombres para variables locales, por ejemplo). Uno debe asegurarse de que tales símbolos no puedan entrar en conflicto con un símbolo que el usuario está usando en el código circundante.
Esto generalmente se logra mediante el uso de GENSYM
, una función que devuelve un nuevo símbolo sin entramar.
Malo
Considere la macro a continuación. Hace un DOTIMES
-loop que también recopila el resultado del cuerpo en una lista, que se devuelve al final.
(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)
Esto parece funcionar en este caso, pero si el usuario tiene un nombre de resultado RESULT
, que utiliza en el cuerpo, los resultados probablemente no sean lo que el usuario espera. Considere este intento de escribir una función que recopile una lista de sumas de todos los enteros hasta N
:
(defun sums-upto (n)
(let ((result 0))
(dotimes+collect (i n)
(incf result i))))
(sums-upto 10) ;=> Error!
Bueno
Para solucionar el problema, necesitamos usar GENSYM
para generar un nombre único para la variable RESULT
en la expansión de macros.
(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: Cómo hacer símbolos a partir de cuerdas.
TODO: Evitar problemas con símbolos en diferentes paquetes.
si-vamos, cuando-dejemos macros de entrada
Estas macros combinan flujo de control y enlace. Son una mejora con respecto a las macros anafóricas anafóricas porque permiten que el desarrollador comunique el significado a través de nombres. Como tal, se recomienda su uso sobre sus contrapartes anafóricas.
(if-let (user (get-user user-id))
(show-dashboard user)
(redirect 'login-page))
FOO-LET
macros FOO-LET
unen una o más variables, y luego usan esas variables como la forma de prueba para el condicional correspondiente ( IF
, WHEN
). Las variables múltiples se combinan con AND
. La rama elegida se ejecuta con los enlaces en vigor. Una implementación simple de una variable de IF-LET
podría ser algo como:
(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.~%")))
Una versión que admite múltiples variables está disponible en la biblioteca de Alexandria .
Usando macros para definir estructuras de datos
Un uso común de las macros es crear plantillas para estructuras de datos que obedecen a reglas comunes pero que pueden contener campos diferentes. Al escribir una macro, puede permitir que se especifique la configuración detallada de la estructura de datos sin necesidad de repetir el código repetitivo, ni usar una estructura menos eficiente (como un hash) en la memoria simplemente para simplificar la programación.
Por ejemplo, supongamos que deseamos definir un número de clases que tienen un rango de propiedades diferentes, cada una con un captador y definidor. Además, para algunas (pero no todas) de estas propiedades, deseamos que el setter llame un método en el objeto que le notifique que la propiedad ha sido cambiada. Aunque Common LISP ya tiene una forma abreviada para escribir getters y setters, escribir un setter personalizado estándar de esta manera normalmente requeriría la duplicación del código que llama al método de notificación en cada setter, lo que podría ser una molestia si hay una gran cantidad de propiedades involucradas. . Sin embargo, al definir una macro es mucho más fácil:
(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)))
Ahora podemos escribir (defclass-notifier-slots foo (bar baz qux) (waldo))
e inmediatamente definir una clase foo
con un slot regular waldo
(creado por la segunda parte de la macro con la especificación (waldo :accessor waldo)
) , y bar
ranuras, baz
y qux
con configuradores que llaman al método changed
(donde el captador se define por la primera parte de la macro, (bar :reader bar)
, y el definidor por la macro del notifier
invocado).
Además de permitirnos definir rápidamente múltiples clases que se comportan de esta manera, con un gran número de propiedades, sin repetición, tenemos el beneficio habitual de la reutilización de código: si más tarde decidimos cambiar la forma en que funcionan los métodos de notificación, simplemente podemos cambiar la macro, y la estructura de cada clase que lo use cambiará.