2012-03-21 17 views
16

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

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 usarlo.

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

+2

vea las explicaciones aquí: https://groups.google.com/forum/?fromgroups#!topic/comp.lang.lisp/F4NVRlOvrX8 –

Respuesta

24

¿usted está 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 comilla inversa anidada, y múltiples niveles que son similares entre sí, lo que lleva a la fácil confusión, incluso para experimentados Codificadores Lisp.

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

Hay un let llano en el cuerpo de la macro sí mismo, entonces una vez-backquoted generó let que vivirá dentro del cuerpo de la macro que usa once-only. Por último, hay una doble backsitio let que aparecerá en la macro expansión de esa macro, en el sitio del código donde el usuario utiliza la macro.

Las dos rondas de generación de gensyms son necesarias porque once-only es una macro en sí misma, por lo que tiene que ser higiénico por sí mismo; por lo que genera un grupo de gensyms por sí mismo en el extremo let. Pero también, el propósito de once-only es simplificar la escritura de otra macro higiénica. Entonces genera gensyms para esa macro también.

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

Si estás escribiendo una macro sencilla, que tienen las variables locales que mantienen gensyms, ej .:

;; 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, que ha inventado un símbolo, counter-sym. Esta variable se define a simple vista. Tú, el humano, lo has elegido de tal manera que no choca con nada en el ámbito léxico. El alcance léxico en cuestión es el de su macro. No tenemos que preocuparnos por counter-sym capturando accidentalmente referencias dentro de count-form o forms porque forms son solo datos que entran en un fragmento de código que terminará insertado en algún ámbito léxico remoto (el sitio donde se usa la macro). Tenemos que preocuparnos por no confundir counter-sym con otra variable dentro de nuestra macro. Por ejemplo, no podemos darle 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 lo ayude a escribir esa macro, la máquina debe hacer el mismo trabajo que usted. Cuando está escribiendo código, tiene que inventar un nombre de variable, y debe tener cuidado con el nombre que inventa.

Sin embargo, la máquina de escribir códigos, a diferencia de usted, no ve el alcance circundante. No puede simplemente observar qué variables existen y elegir las que no chocan. 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 la máquina ha 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: gensimos.

Continuando con el ejemplo, supongamos que tenemos un robot que escribirá este macro cuerpo 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 sería el robot macro parece?

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

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

Por supuesto, si solo quisiéramos que un robot escribiera tiempos de repetición, lo convertiríamos en una función, y entonces esa función no tendría que preocuparse por la invención de variables: no es una macro y por lo tanto no funciona t necesidad de la 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-onlyno puede ser una función porque su trabajo es inventar las variables en nombre de su jefe, la macro que lo utiliza, y una función no se puede introducir variables en su llamador.

+4

P.S. puedes agradecer la notación de la contrafija para hacer que este tipo de cosas sea factible. Sin backquote, 'una vez-solo' sería horrendo. Solo alguien con capacidades de "idiot savant" en la programación de Lisp podría ver a través de él. La notación de backquote es el verdadero caballo de batalla en la macro escritura de Lisp. – Kaz

+2

* La sintaxis de backquote fue particularmente poderosa cuando se anidaba. Esto se produjo principalmente dentro de las macro macroplicaciones ; debido a que tales fueron codificados principalmente por los asistentes, la capacidad de escribir y interpretar expresiones anidadas de la comilla atrás pronto estuvo rodeada por una cierta mística. Alan Bawden de MIT adquirió una reputación particular como backquote-meister en los primeros días de la Máquina Lisp. * - "La Evolución de Lisp", Gabriel, Steele. – Kaz

+3

* Backquote y DEFMACRO hicieron una gran diferencia. Este salto en el poder expresivo, puesto a disposición en una forma estándar, comenzó una nueva oleada de extensión de lenguaje porque ahora era mucho más fácil definir nuevas construcciones lingüísticas de forma estándar y portátil para que los dialectos experimentales pudieran ser compartidos. * " La evolución de Lisp ", Gabriel, Steele. – Kaz

7

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

2

Kaz lo explicó de manera hermosa y extensa.

Sin embargo, si no le importa mucho el problema de doble higiene, es posible encontrar este 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 let se backquoted dos veces, porque será parte de la expansión final. El objetivo es evaluar las formas en los símbolos enlazados originales en enlaces nuevos.

El segundo let se vuelve a poner una vez, porque formará parte del usuario de once-only. El propósito es volver a unir los símbolos originales a los símbolos nuevos, ya que sus formularios se habrán evaluado y ligado a ellos en la expansión final.

Si el reenlace de los símbolos originales estaba antes de la expansión de macros final, la expansión de macro final se referiría a los símbolos no internados en lugar de a las formas originales.

Una implementación de with-slots que utiliza once-only es un ejemplo que requiere de 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 interior let se shadowing la unión a la variable #:obj del outter let. Por lo tanto, acceder al a dentro de with-slots interno realmente accedería al segundo objeto.

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

Cuestiones relacionadas