2010-02-10 3 views
6

J'ai écrit quelques cas de test simples pour l'une de mes missions, et j'ai développé une suite de tests en utilisant des macros. J'ai run-test et run-test-section et ainsi de suite. Je voudrais run-test-section pour prendre un certain nombre de paramètres qui sont run-test invocations et compte le nombre de PASS et d'échecs.Comment écrire un appel de macro récursif sur un paramètre & REST dans Lisp?

run-test renvoie T sur PASS et NIL sur FAIL. Ce que je cherche à faire maintenant est d'écrire une macro qui prend un paramètre &REST, et invoque chacun des éléments de cette liste, retournant finalement le nombre de valeurs TRUE.

C'est ce que j'ai actuellement:

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

Cependant cela met mon REPL dans une boucle infinie. Est-ce que quelqu'un pourrait indiquer comment je peux manipuler plus efficacement les arguments? Est-ce même une bonne idée? Est-ce qu'il y a une meilleure approche?

modifier:

Comme il est indiqué dans les réponses, une macro n'est pas nécessaire dans ce cas. En utilisant le COUNT intégré suffira. Cependant, il y a des informations utiles dans les réponses sur les appels de macro récursifs.

Répondre

5

Au moment de l'expansion macro, cdr n'est pas évaluée. Alors (count-true t t nil) frappe une expansion infinie comme ceci:

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

Eh bien, en fait cela se produit pour les deux branches récursives à la fois. Donc, il explose encore plus vite que l'exemple.

Une meilleure idée?

  1. Essayer d'écrire la même chose qu'une fonction en premier. Déterminez où vous devez mettre les lambdas pour retarder l'évaluation. Ensuite, faites abstraction de la fonction dans une macro afin de pouvoir omettre les lambdas. Le fait d'écrire une fonction d'abord, d'ailleurs, c'est que parfois vous trouverez qu'une fonction est assez bonne. Ecrire une macro où une fonction ferait une erreur.

  2. Généralement, lorsque vous écrivez des macros dans Common Lisp, commencez par loop plutôt que par récursivité. Les macros récursives sont délicates (et généralement fausses :).

Edit:

Voici un exemple plus correct (mais beaucoup plus):

(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

Merci pour l'explication. Je vois maintenant ma compréhension erronée de l'expansion macro. bon conseil sur l'utilisation de 'LOOP' à la place. ive depuis compris comment utiliser juste une fonction pour cette tâche. –

3

Le problème est que votre macro se développe en une forme infinie. Le système essaiera de tout développer pendant la phase de macro-expansion et devra donc manquer de mémoire.

Notez que le test pour la fin de la liste des formulaires n'est jamais évalué mais exprimé en tant que code. Il devrait être en dehors de l'expression backtick. Et comme l'autre personne l'a expliqué, les (cdr forms) doivent également être évalués au moment de la macro-expansion, et non pas comme du code à compiler.

I.e. quelque chose comme ça (non testé):

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

Oublier macros récursives. Ils sont pénibles et vraiment seulement pour les utilisateurs avancés de Lisp.

Une version simple, non récursif:

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

Eh bien, voici une macro récursive:

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

Pourquoi pas 'count, form' au lieu de' + 'et' collect'? – Svante

+0

Parce que la boucle ne compte pas les résultats. Il génère du code. Il est parti après l'expansion. –

+0

merci pour la réponse. son direct et répond à la question de travailler avec & rest paramètres dans une macro récursive. –

2

Je crois que vous êtes sous deux fausses impressions sur ce que les macros sont.

Les macros sont écrites pour l'expansion, les fonctions pour l'exécution. Si vous écrivez une macro récursive, elle se développera de manière récursive, sans exécuter quoi que ce soit du code qu'elle produit. Une macro est pas du tout quelque chose comme une fonction inline! Lorsque vous écrivez une macro, elle peut s'étendre aux appels de fonction. Lorsque vous écrivez une macro, vous faites pas s'aventurer dans "macro land" où les fonctions ne seraient pas disponibles. Cela n'a aucun sens d'écrire count-true comme une macro.

+1

l'idée d'une macro récursive est généralement qu'elle se développe en code, qui utilise à nouveau cette macro, mais sur des arguments plus simples/plus petits. La macro-expansion s'arrête lorsque la macro est utilisée dans sa forme la plus simple. Donc, la récursion se fait via le mécanisme de macro expansion ... –

+0

merci d'avoir redressé la tête. j'ai compris comment accomplir ce que je cherchais en utilisant juste une fonction. J'avais l'impression que je devais utiliser une macro à cause de la façon dont je pensais interagir avec les expressions évaluées. maintenant je vois comment je me suis trompé. –

0

Comme d'autres l'ont dit, évitez les macros récursives. Si vous êtes prêt à le faire dans une fonction, vous pouvez utiliser apply:

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

Ceci est une application agréable pour récursion macro facile:

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

Tests:

[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 doit raisonner sur l'entrée syntaxe et générer le code qui effectue le comptage; vous ne devez pas confondre entre générer le code et évaluer.

Vous allez mal dès le départ quand vous faites ceci:

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

vous poussez le calcul de méta-syntaxique (combien de formes-nous dans la syntaxe?) Dans la Le code généré doit être évalué lors de l'exécution. Vous avez les bonnes pièces dans ce cas de base, mais ils sont mal agencés. Dans ma solution, je le cond juste dans le corps de la macro elle-même, et non pas dans un backquote:

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

En gros:

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

Si vous ne savez pas quoi faire, écrire le code que vous voulez que la macro écrive.Par exemple:

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

Ok, donc pour ce cas précis, nous écrire:

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

Terminé! Maintenant supposons que nous voulons soutenir (count-true) avec aucune forme du tout.

Wanted Syntax     Translation 

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

Quand il y a une forme, le reste l'expansion if, mais quand il n'y a pas de formes, nous voulons juste un zéro constant. Facile, faire l'argument optionnel:

(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 

Enfin, étendre à la forme N-aire:

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

Mais! nous notons maintenant que les termes soulignés correspondent à la sortie du cas unique.

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

qui généralise

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

Cela correspond d'une manière simple au modèle de génération de code avec car/cdr récursion:

`(+ (count-true ,CAR) (count-true ,*CDR)) 
Questions connexes