J'aime les macros.
Voici un code qui permet d'extraire les attributs des personnes à partir de LDAP. Il se trouve que j'avais ce code qui traînait et j'ai pensé qu'il pourrait être utile à d'autres.
Certaines personnes sont confuses quant à une supposée pénalité d'exécution des macros, j'ai donc ajouté une tentative de clarification à la fin.
Au début, il y avait la duplication
(defun ldap-users ()
(let ((people (make-hash-table :test 'equal)))
(ldap:dosearch (ent (ldap:search *ldap* "(&(telephonenumber=*) (cn=*))"))
(let ((mail (car (ldap:attr-value ent 'mail)))
(uid (car (ldap:attr-value ent 'uid)))
(name (car (ldap:attr-value ent 'cn)))
(phonenumber (car (ldap:attr-value ent 'telephonenumber))))
(setf (gethash uid people)
(list mail name phonenumber))))
people))
Vous pouvez considérer un "let binding" comme une variable locale, qui disparaît en dehors de la forme LET. Remarquez la forme des liaisons -- elles sont très similaires, ne différant que par l'attribut de l'entité LDAP et le nom ("variable locale") auquel lier la valeur. Utile, mais un peu verbeux et contenant des doublons.
La quête de la beauté
Ne serait-il pas agréable de ne pas avoir à faire toutes ces répétitions ? Un idiome commun est la macro WITH-..., qui lie les valeurs en fonction d'une expression dont vous pouvez extraire les valeurs. Introduisons notre propre macro qui fonctionne de cette manière, WITH-LDAP-ATTRS, et remplaçons-la dans notre code original.
(defun ldap-users ()
(let ((people (make-hash-table :test 'equal))) ; equal so strings compare equal!
(ldap:dosearch (ent (ldap:search *ldap* "(&(telephonenumber=*) (cn=*))"))
(with-ldap-attrs (mail uid name phonenumber) ent
(setf (gethash uid people)
(list mail name phonenumber))))
people))
Avez-vous vu comment un groupe de lignes a soudainement disparu, et a été remplacé par une seule ligne ? Comment faire cela ? En utilisant des macros, bien sûr - du code qui écrit du code ! Les macros en Lisp sont un animal totalement différent de celles que vous pouvez trouver en C/C++ grâce à l'utilisation du pré-processeur : ici, vous pouvez exécuter réel Le code Lisp (pas le #define
fluff in cpp) qui génère du code Lisp, avant que l'autre code ne soit compilé. Les macros peuvent utiliser n'importe quel code Lisp réel, c'est-à-dire des fonctions ordinaires. Essentiellement aucune limite.
Se débarrasser du laid
Voyons donc comment cela a été fait. Pour remplacer un attribut, nous définissons une fonction.
(defun ldap-attr (entity attr)
`(,attr (car (ldap:attr-value ,entity ',attr))))
La syntaxe de la citation inverse est un peu compliquée, mais son utilité est simple. Lorsque vous appelez LDAP-ATTRS, il vous enverra une liste contenant les éléments suivants valeur de attr
(c'est la virgule), suivi de car
("premier élément de la liste" (paire de cons, en fait), et il existe en fait une fonction appelée first
que vous pouvez aussi utiliser), qui reçoit la première valeur de la liste retournée par ldap:attr-value
. Parce que ce n'est pas le code que nous voulons exécuter quand nous compilons le code (obtenir les valeurs d'attribut est ce que nous voulons faire quand nous ejecute le programme), nous n'ajoutons pas de virgule avant l'appel.
Bref. Passons au reste de la macro.
(defmacro with-ldap-attrs (attrs ent &rest body)
`(let ,(loop for attr in attrs
collecting `,(ldap-attr ent attr))
,@body))
El ,@
La syntaxe consiste à mettre le contenu d'une liste quelque part, au lieu de la liste elle-même.
Résultat
Vous pouvez facilement vérifier que cela vous donnera la bonne chose. Les macros sont souvent écrites de cette manière : vous commencez par le code que vous voulez simplifier (la sortie), ce que vous voulez écrire à la place (l'entrée), puis vous commencez à modeler la macro jusqu'à ce que votre entrée donne la sortie correcte. La fonction macroexpand-1
vous dira si votre macro est correcte :
(macroexpand-1 '(with-ldap-attrs (mail phonenumber) ent
(format t "~a with ~a" mail phonenumber)))
évalue à
(let ((mail (car (trivial-ldap:attr-value ent 'mail)))
(phonenumber (car (trivial-ldap:attr-value ent 'phonenumber))))
(format t "~a with ~a" mail phonenumber))
Si vous comparez les liaisons LET de la macro développée avec le code du début, vous constaterez qu'il s'agit de la même forme !
Compilation et exécution : Macros et fonctions
Une macro est un code qui est exécuté à temps de compilation avec l'avantage supplémentaire qu'ils peuvent appeler n'importe qui. ordinaire ou macro comme bon leur semble ! Ce n'est pas beaucoup plus qu'un filtre fantaisiste, prenant quelques arguments, appliquant quelques transformations et fournissant ensuite au compilateur les s-exps résultantes.
En gros, il vous permet d'écrire votre code en verbes que l'on peut trouver dans le domaine du problème, au lieu de primitives de bas niveau du langage ! A titre d'exemple, considérez ce qui suit (si when
n'était pas déjà intégré) : :
(defmacro my-when (test &rest body)
`(if ,test
(progn ,@body)))
if
est une primitive intégrée qui vous permettra seulement d'exécuter un dans les branches, et si vous voulez en avoir plus d'une, eh bien, vous devez utiliser le formulaire progn
: :
;; one form
(if (numberp 1)
(print "yay, a number"))
;; two forms
(if (numberp 1)
(progn
(assert-world-is-sane t)
(print "phew!"))))
Avec notre nouvel ami, my-when, nous pourrions à la fois a) utiliser le verbe le plus approprié si nous n'avons pas de fausse branche, et b) ajouter un opérateur de séquencement implicite, c'est-à-dire progn
: :
(my-when (numberp 1)
(assert-world-is-sane t)
(print "phew!"))
Le code compilé ne contiendra jamais my-when
Cependant, parce que lors de la première passe, toutes les macros sont développées et il y a donc pas de pénalité de temps d'exécution impliqué !: :
Lisp> (macroexpand-1 '(my-when (numberp 1)
(print "yay!")))
(if (numberp 1)
(progn (print "yay!")))
Notez que macroexpand-1
ne fait qu'un seul niveau d'extensions ; il est possible (très probable, en fait !) que les extensions continuent plus bas. Cependant, vous finirez par atteindre les détails d'implémentation spécifiques au compilateur qui ne sont souvent pas très intéressants. Mais en continuant à développer le résultat, vous obtiendrez soit plus de détails, soit simplement votre s-exp d'entrée.
J'espère que cela clarifie les choses. Les macros sont un outil puissant, et l'une des fonctionnalités de Lisp que j'apprécie.