for dummies declarar common macros lisp common-lisp practical-common-lisp

macros - dummies - declarar variables en lisp



Entendiendo cómo implementar una sola macro lisp (3)

En el libro "Practical Common Lisp" de Peter Seibel, podemos encontrar la definición de la macro muy complicada una sola vez (consulte la parte inferior de la página http://www.gigamonkeys.com/book/macros-defining-your-own.html ).

Estoy leyendo esta definición de macro por décima vez en las últimas 3 semanas y no puedo entender cómo funciona. :( Peor aún, no puedo desarrollar esta macro por mi cuenta, aunque entiendo su propósito y cómo usarla.

Estoy especialmente interesado en la "derivación" sistemática de esta macro notoriamente difícil, ¡paso a paso! ¿Alguna ayuda?


¿Estás mirando esto?

(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)))))

No es tan complicado, pero tiene una cita anidada, y múltiples niveles que son similares entre sí, lo que lleva a una fácil confusión, incluso para los codificadores Lisp experimentados.

Esta es una macro que las macros utilizan para escribir sus expansiones: una macro que escribe partes de los cuerpos de las macros.

Hay una simple let en el cuerpo de la macro en sí misma, luego una llamada generada una vez atrás que vivirá dentro del cuerpo de la macro que solo se usa once-only . Finalmente, hay un doble let atrás entre comillas que aparecerá en la expansión de macro de esa macro, en el sitio de código donde el usuario utiliza la macro.

Las dos rondas de gensyms generadores son necesarias porque once-only es una macro en sí misma, y ​​por lo tanto tiene que ser higiénica por sí misma; por lo que genera un montón de gensyms para sí mismo en el extremo más exterior. Pero también, el propósito de once-only es simplificar la escritura de otra macro higiénica. Así que genera gensyms para esa macro también.

En pocas palabras, once-only necesita crear una macroexpansión que requiere algunas variables locales cuyos valores son gensyms. Esas variables locales se utilizarán para insertar los gensyms en otra expansión macro para que sea higiénico. Y esas variables locales tienen que ser higiénicas ya que son una macro expansión, por lo que también son gensyms.

Si está escribiendo una macro simple, tiene variables locales que contienen gensyms, por ejemplo:

;; silly example (defmacro repeat-times (count-form &body forms) (let ((counter-sym (gensym))) `(loop for ,counter-sym below ,count-form do ,@forms)))

En el proceso de escribir la macro, has inventado un símbolo, counter-sym . Esta variable se define en la vista llana. Tú, el humano, lo has elegido de tal manera que no choca con nada en el ámbito léxico. El ámbito léxico en cuestión es el de tu macro. No tenemos que preocuparnos por que el counter-sym capture accidentalmente las referencias dentro count-form o los forms porque los forms son solo datos que ingresan en un fragmento de código que terminará insertado en algún ámbito léxico remoto (el sitio donde se encuentra la macro). usado). Tenemos que preocuparnos por no confundir el counter-sym con otra variable dentro de nuestra macro. Por ejemplo, no podemos dar a nuestra variable local el nombre count-form . ¿Por qué? Porque ese nombre es uno de nuestros argumentos de función; Lo seguiríamos, creando un error de programación.

Ahora, si desea que una macro le ayude a escribir esa macro, entonces la máquina tiene que hacer el mismo trabajo que usted. Cuando está escribiendo código, tiene que inventar un nombre de variable, y tiene que tener cuidado con el nombre que inventa.

Sin embargo, la máquina de escritura de códigos, a diferencia de usted, no ve el ámbito circundante. No se puede simplemente mirar qué variables están ahí y elegir las que no coinciden. La máquina es solo una función que toma algunos argumentos (piezas de código no evaluado) y produce una pieza de código que luego se sustituye ciegamente en un alcance después de que esa máquina haya hecho su trabajo.

Por lo tanto, la máquina tiene que elegir los nombres extra sabiamente. De hecho, para ser completamente a prueba de balas, tiene que ser paranoico y usar símbolos que son completamente únicos: gensyms.

Continuando con el ejemplo, supongamos que tenemos un robot que escribirá este cuerpo de macro para nosotros. Ese robot puede ser una macro, repeat-times-writing-robot :

(defmacro repeat-times (count-form &body forms) (repeat-times-writing-robot count-form forms)) ;; macro call

¿Cómo podría ser la macro robot?

(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))))

Puede ver cómo esto tiene algunas de las características de once-only : el anidamiento doble y los dos niveles de (gensym) . Si puedes entender esto, entonces el salto a once-only es pequeño.

Por supuesto, si solo quisiéramos que un robot escribiera repetidas veces, lo convertiríamos en una función, y luego esa función no tendría que preocuparse por inventar variables: no es una macro y, por lo tanto, no necesita higiene:

;; 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

Pero once-only no puede ser una función porque su trabajo es inventar variables en nombre de su jefe, la macro que la usa, y una función no puede introducir variables en su interlocutor.


Kaz lo explicó bellamente y extensamente.

Sin embargo, si no te importa mucho el problema de la doble higiene, este podría ser más fácil de entender:

(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))))

La primera vez let hay se vuelve a cotizar dos veces, porque será parte de la expansión final. El propósito es evaluar las formas en los símbolos originales enlazados en enlaces nuevos.

La segunda let es una cita inversa, ya que será parte del usuario once-only . El propósito es volver a unir los símbolos originales con los símbolos nuevos, ya que sus formas se habrán evaluado y enlazado en la expansión final.

Si la revinculación de los símbolos originales era anterior a la expansión de la macro final, la expansión de la macro final se referiría a los símbolos no incluidos en lugar de las formas originales.

Una implementación de with-slots que usa once-only una once-only es un ejemplo que requiere doble higiene:

(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))))

La segunda expansión muestra que el margen interno es el sombreado del enlace a la variable #:obj del margen externo. Por lo tanto, acceder a a dentro de las with-slots internas with-slots realidad tendría acceso al segundo objeto.

Tenga en cuenta que en este ejemplo, la macroexpansión externa obtiene un gensym llamado g2 y el g1 interno. En la evaluación o compilación normal, sería todo lo contrario, ya que las formas se caminan de lo externo a lo interno.


Una alternativa a la macro de once-only de Practical Common Lisp se deriva de Let Over Lambda (consulte la sección ''Una vez'' en el tercer capítulo).