2010-02-10 9 views
6

He estado escribiendo algunos casos de prueba simples para una de mis tareas, y he desarrollado un poco de un conjunto de pruebas usando macros. Tengo run-test y run-test-section y más. Me gustaría run-test-section para tomar una serie de parámetros que son invocaciones run-test y contar el número de PASSes y FAILs.Cómo escribir una llamada de macro recursiva en un parámetro & REST en Lisp?

run-test devuelve T en PASS, y NIL en FAIL.

Lo que estoy buscando hacer ahora es escribir una macro que toma un parámetro &REST, e invoca cada uno de los elementos de esta lista, finalmente devolviendo el número de valores VERDADEROS.

Esto es lo que tengo actualmente:

(defmacro count-true (&rest forms) 
`(cond 
    ((null ,forms) 
     0) 
    ((car ,forms) 
     (1+ (count-true (cdr ,forms)))) 
    (T 
     (count-true (cdr ,forms))))) 

Sin embargo, esto pone mi REPL en un bucle infinito. ¿Podría alguien señalar cómo puedo manipular de manera más efectiva los argumentos? ¿Es esto incluso una buena idea? ¿Hay un mejor enfoque?

edición:

Como se observa en las respuestas, no se necesita una macro en este caso. Usar el COUNT incorporado será suficiente. Sin embargo, hay información útil en las respuestas sobre macro llamadas recursivas.

Respuesta

5

A la hora de la expansión de macros, cdr no se evalúa. Así (count-true t t nil) golpea una expansión infinita de esta manera:

(count-true t t nil) 
=> 
(1+ (count-true (cdr (t t t nil)))) 
=> 
(1+ (1+ (count-true (cdr (cdr (t t t nil)))))) 
=> 
(1+ (1+ (1+ (count-true (cdr (cdr (cdr (t t t nil)))))))) 
=> ... 

Bueno, en realidad esto sucede por ambas ramas recursivas a la vez. Entonces explota aún más rápido que el ejemplo.

¿Una idea mejor?

  1. Primero intenta escribir lo mismo como una función. Averigua dónde tienes que poner las lambdas para retrasar la evaluación. Luego, abstrae la función en una macro para que puedas omitir las lambdas.

    El punto de escribir una función primero, por cierto, es que a veces encontrará que una función es lo suficientemente buena. Escribir una macro donde una función haría es un error.

  2. Generalmente, cuando escribe macros en Common Lisp, comience con loop en lugar de recursividad. Las macros recursivas son complicadas (y generalmente son incorrectas :).

Editar:

Aquí es un ejemplo más correcta (pero mucho más larga):

(count-true t nil) => 
(cond 
    ((null '(t nil)) 0) 
    ((car '(t nil)) (1+ (count-true (cdr '(t nil))))) 
    (T (count-true (cdr '(t nil))))) 
=> 
(cond 
    ((null '(t nil)) 0) 
    ((car '(t nil)) (1+ (1+ (count-true (cdr (cdr '(t nil))))))) 
    (T (count-true (cdr (cdr '(t nil)))))) 
=> 
(cond 
    ((null '(t nil)) 0) 
    ((car '(t nil)) (1+ (1+ (1+ (count-true (cdr (cdr (cdr '(t nil))))))))) 
    (T (count-true (cdr (cdr (cdr '(t nil))))))) 
=> 
(cond 
    ((null '(t nil)) 0) 
    ((car '(t nil)) (1+ (1+ (1+ (1+ (count-true (cdr (cdr (cdr (cdr '(t nil))))))))))) 
    (T (count-true (cdr (cdr (cdr (cdr '(t nil)))))))) 
+0

gracias por la explicación. Ahora veo mi comprensión errónea de la macro expansión. un buen consejo sobre el uso de 'LOOP' en su lugar. Desde que descubrí cómo usar solo una función para esta tarea. –

3

El problema es que su macro se expande a una forma infinita. El sistema intentará expandirlo todo durante la fase de expansión de macro y, por lo tanto, debe quedarse sin memoria.

Tenga en cuenta que la prueba para el final de la lista de formularios nunca se evalúa sino que se expresa como código. Debería estar fuera de la expresión de retroceso. Y como explicó la otra persona, los formularios (cdr) deben evaluarse también en el momento de la expansión macro, no dejarse como código para compilar.

I.e. algo como esto (no probado):

(defmacro count-true (&rest forms) 
    (if forms 
     `(if (car ',forms) 
      (1+ (count-true ,@(cdr forms))) 
     (count-true ,@(cdr forms))) 
    0)) 
4

Forget macros recursivas. Son un dolor y realmente solo para usuarios avanzados de Lisp.

Una versión simple, no recursivo:

(defmacro count-true (&rest forms) 
    `(+ 
    ,@(loop for form in forms 
      collect `(if ,form 1 0)))) 

CL-USER 111 > (macroexpand '(count-true (plusp 3) (zerop 2))) 
(+ (IF (PLUSP 3) 1 0) (IF (ZEROP 2) 1 0)) 

Bueno, aquí es una macro recursiva:

(defmacro count-true (&rest forms) 
    (if forms 
     `(+ (if ,(first forms) 1 0) 
      (count-true ,@(rest forms))) 
    0)) 
+0

¿Por qué no 'count, form' en lugar de' + 'y' collect'? – Svante

+0

Porque el LOOP no cuenta los resultados. Genera código. Se va después de la expansión. –

+0

gracias por la respuesta. es directo y responde la pregunta de trabajar con parámetros & rest en una macro recursiva. –

2

Creo que usted es menor de dos impresiones falsa con respecto a lo que son las macros.

Las macros se escriben para funciones de expansión y ejecución. Si escribe una macro recursiva, se expandirá recursivamente, sin ejecutar nada del código que produce. Una macro es para nada ¡algo así como una función en línea!

Cuando escribe una macro, puede expandirse a llamadas de función. Cuando escribe una macro, hace no incursiona en "macro tierra" donde las funciones no estarían disponibles. No tiene sentido escribir count-true como una macro.

+1

la idea de una macro recurrente suele ser que se expande en código, que usa esa macro nuevamente, pero en argumentos más simples/más pequeños. La macro expansión se detiene cuando la macro se usa en su forma más simple. Así que la recursión se realiza a través del mecanismo de expansión macro ... –

+0

gracias por tener mi cabeza recta. descubrí cómo lograr lo que estaba buscando usando solo una función. tenía la impresión de que tenía que usar una macro debido a la forma en que pensaba que estaba interactuando con las expresiones evaluadas. ahora veo cómo me equivoqué. –

0

Como han dicho otros, evite las macros recursivas. Si usted está dispuesto a hacer esto en una función, puede utilizar apply:

(defun count-true (&rest forms) 
    (cond 
    ((null forms) 0) 
    (t (+ 1 (apply #'count-true (cdr forms)))))) 
1

Esta es una buena aplicación para facilitar la recursión de macros:

(defmacro count-true (&rest forms) 
    (cond 
    ((null forms) 0) 
    ((endp (rest forms)) `(if ,(first forms) 1 0)) 
    (t `(+ (count-true ,(first forms)) (count-true ,@(rest forms)))))) 

Pruebas:

[2]> (count-true) 
0 
[3]> (count-true nil) 
0 
[4]> (count-true t) 
1 
[5]> (count-true nil t) 
1 
[6]> (count-true t nil) 
1 
[7]> (count-true t t) 
2 
[8]> (count-true nil nil) 
0 
[9]> (macroexpand '(count-true)) 
0 ; 
T 
[10]> (macroexpand '(count-true x)) 
(IF X 1 0) ; 
T 
[11]> (macroexpand '(count-true x y)) 
(+ (COUNT-TRUE X) (COUNT-TRUE Y)) ; 
T 
[12]> (macroexpand '(count-true x y z)) 
(+ (COUNT-TRUE X) (COUNT-TRUE Y Z)) ; 
T 

La macro tiene que razonar sobre la entrada sintaxis y generar el código que hace el recuento; no debe mezclarse entre generar el código y evaluar.

Vas mal la derecha del palo cuando estás haciendo esto:

`(cond ((null ,forms ...) ...) 

que está empujando el cálculo meta-sintáctica (cuántas formas tenemos en la sintaxis?) En el la plantilla de código generada se evaluará en tiempo de ejecución. Tienes las piezas correctas en este caso base, pero están mal organizadas. En mi solución, tengo el cond sólo en el propio cuerpo de la macro, no en una comilla inversa:

(cond ((null forms) ...) ...) 

Básicamente:

(cond (<if the syntax is like this> <generate this>) 
     (<if the syntax is like that> <generate that>) 
     ...) 

Si usted no sabe qué hacer, escribe el código que quieres que la macro escribaPor ejemplo:

;; I want this semantics: 
(if (blah) 1 0) ;; count 1 if (blah) is true, else 0 

;; But I want it with this syntax: 
(count-true (blah)) 

bien, así que para este caso exacto, escribiríamos:

(defmacro count-true (single-form) 
    `(if ,single-form 1 0)) 

hecho! Ahora supongamos que queremos admitir (count-true) sin formularios.

Wanted Syntax     Translation 

(count-true)     0 
(count-true x)     (if x 1 0) 

Cuando hay una forma, la expansión if se queda, pero cuando no hay formas, sólo queremos un cero constante. Fácil, hacer que el argumento opcional:

(defmacro count-true (&optional (single-form nil have-single-form)) 
    (if have-single-form 
    `(if ,single-form 1 0) ;; same as before 
     0))     ;; otherwise zero 

Por último, se extiende a la forma de N-aria:

Wanted Syntax     Translation 

(count-true)     0 
(count-true x)     (if x 1 0) 
(count-true x y)    (+ (if x 1 0) (if y 1 0)) 
            ^^^^^^^^^^ ^^^^^^^^^^ 

Pero! ahora notamos que los términos subrayados corresponden a la salida del caso individual.

(count-true x y)    (+ (count-true x) (count-true y)) 

que generaliza

(count-true x y z ...)   (+ (count-true x) (count-true y z ...)) 

que corresponde de manera directa a la plantilla de generación de código con car/cdr recursividad:

`(+ (count-true ,CAR) (count-true ,*CDR)) 
Cuestiones relacionadas