Cet article a pour but de présenter les macros de Common Lisp au travers d'un exemple concret, qui permettra peut-être à certaines personnes de mieux comprendre quand les utiliser, ainsi que leur intérêt.
Si vous avez déjà touché à clojure ou à arc, vous aurez sûrement apprécié leurs raccourcis pour écrire des fonctions anonymes. Par exemple en Clojure, les deux s-exps suivantes sont équivalentes:
(map (fn [x] (println "Hello, ")) '("world!" "you!"))
(map #(println "Hello, " %) '("world!" "you!"))
Avec arc, c'est le même principe (ça sera [prn "Hello, " _]), sauf
qu'on a pas la possiblité d'utiliser ce mécanisme avec plusieurs
arguments, tandis qu'avec clojure il est possible d'utiliser %1,
%2 pour indiquer le premier argument, le second argument, …
Comme cette fonctionnalitée n'est pas présente dans Common Lisp, nous allons donc l'ajouter grâce aux macros.
Titre
La première chose à faire avant d'écrire une macro est d'écrire un ou deux exemples d'utilisation de cette macro, afin de clarifier nos idées. Comme la syntaxe '#(...)' est déjà utilisée pour les vecteurs, nous allons utiliser des crochets pour délimiter les fonction anonymes raccourcies, comme dans arc, mais nous allons garder le symbole pourcent suivit d'un nombre de clojure pour identifier les paramètres. Ainsi, l'exemple précédent s'écrira comme suit en Common Lisp grâce à notre macro:
(mapcar [format t "Hello, ~a~%" %] '("world!" "you!"))
Et sera équivalent à:
(mapcar (lambda (x) (format t "Hello, ~a~%" x)) '("world!" "you!"))
Mais premièrement, on va laisser les crochets de côté et écrire une macros qui sera équivalente. On verra à la fin de cet article comment faire pour manipuler le code avant le reader et donc pouvoir utiliser les crochets. Contentons nous pour le moment de manipuler le code après le reader et avant l'évaluation.
Première version simplifiée
Dans le développement de macros, il est préférable de coder la macros
en plusieurs étapes. Nous allons donc écrire une première version de
fn (notre macro), qui ne prendra pas en compte le nombre et l'ordre
de ses arguments (le premier qui apparaîtra dans le code sera le
premier argument, peu importe son numéro, et (fn (1+ %3)) ne prendra
qu'un argument plutôt que trois).
Pour récupérer tous les symboles commençant par un % au sein de
notre macro, nous auront besoin des fonctions flatten (voir On
Lisp, p. 49) et
percent-symbol-p. La première, très utile pour les macros, « aplatit
» une liste, par exemple:
> (flatten '(1 (2 (3 4) 5) 6))
(1 2 3 4 5 6)
La seconde permet simplement de savoir si un symbole commence par un
%:
> (percent-symbol-p '%foo)
T
> (percent-symbol-p 'foo)
NIL
Voici leur définitions:
(defun flatten (x)
(labels ((rec (x acc)
(cond ((null x) acc)
((atom x) (cons x acc))
(t (rec
(car x)
(rec (cdr x) acc))))))
(rec x nil)))
(defun percent-symbol-p (sym)
(string= (subseq (symbol-name sym) 0 1)
"%"))
On peut alors définir fn assez simplement: on récupère tous les
symboles commençant par %, et on encapsule le code dans un lambda
qui prend les symboles récupérés comme arguments:
(defmacro fn (code)
(let ((args
(remove-duplicates
(remove-if-not #'percent-symbol-p
(flatten code)))))
`(lambda ,args
,code)))
Il est donc déjà possible d'utiliser cette macro:
> (mapcar (fn (1+ %)) '(1 2 3))
(2 3 4)
Une version plus correcte
Maintenant que l'on a une macro de base qui fonctionne, on peut commencer à régler ses défauts petit à petit. La façon la plus simple pour gérer l'ordre des symboles et prendre en argument des symboles qui n'apparaissent pas dans le code et de récupérer l'argument maximal et de générer la liste de tous les arguments inférieurs.
On va donc avoir besoin de quelques fonctions supplémentaires:
mkstr(On Lisp, p. 58) qui transforme tous ses arguments enstringsymb(On Lisp, p. 58) qui convertit la chaîne de caracère formé par ses arguments ensymbolpercent-valuequi permet de connaître la valeur d'un symbole commençant par%(par exemple,%et%1valent 1 et%3vaut 3)gen-percents-symsqui génère tous les symboles pourcents entre 1 et la valeur maximale
(defun mkstr (&rest args)
(with-output-to-string (s)
(dolist (a args) (princ a s))))
(defun symb (&rest args)
(values (intern (apply #'mkstr args))))
(defun percent-value (sym)
(if (symbolp sym)
(let ((sym-name (symbol-name sym)))
(handler-case
(if (string= (subseq sym-name 0 1) "%")
(if (= (length sym-name) 1)
1
(parse-integer (subseq sym-name 1)))
0)
(parse-error () 0)))
0))
(defun gen-percents-syms (max)
(loop for i from 1 to max
collect (symb "%" i)))
La nouvelle macro se définit alors:
(defmacro fn (code)
(let ((arg-max (loop for sym in (flatten code)
maximize (percent-value sym))))
`(lambda ,(gen-percents-syms arg-max)
,code)))
Finalisation
Notre macro est presque complète, il ne nous reste plus que deux cas à
gérer:
- clojure permet aussi d'utiliser %& pour définir des arguments de
type &rest
- il faut que % soit équivalent à %1
Cela se fait assez facilement:
(defun replace-symbol (symbol1 symbol2 list)
(typecase list
(list (mapcar (lambda (x) (replace-symbol symbol1 symbol2 x))
list))
(t (if (eq list symbol1)
symbol2
list))))
(defun gen-percents-and-rest (max restp)
(if restp
(append (gen-percents-syms max) '(&rest %&))
(gen-percents-syms max)))
(defmacro fn (code)
(let ((arg-max (loop for sym in (flatten code)
maximize (percent-value sym)))
(restp (find '%& (flatten code))))
`(lambda ,(gen-percents-and-rest arg-max restp)
,(replace-symbol '% '%1 code))))
Ajout des crochets
Nous allons maintenant avoir recours aux read-macros pour modifier le code
avant que le reader ne le lise. Tout ce que notre read-macro a à
faire ici, c'est de transformer une expression du type [...] en (fn
(...)). On définit donc une fonction qui sera appelée chaque fois
qu'un crochet ouvrant est rencontré dans le code, qui se charge de
lire le code jusqu'au crochet fermant et de retourner le code lu.
Le premier argument d'une telle fonction est le flux depuis lequel
lire le code, et le second correspond au caractère qui a déclenché son
appel. Comme nous ne l'utilisons que pour un [, on peut ignorer ce
second argument.
(defun bracket-reader (stream c)
(declare (ignore c))
(let (chars)
(do ((char (read-char stream) (read-char stream)))
((char= char #\]))
(push char chars))
(read-from-string
(format nil "(fn (~a))" (coerce (nreverse chars) 'string)))))
Et on définit finalement [ comme macro-character:
(set-macro-character #\[ #'bracket-reader)
Conclusion
Voilà, au travers de cet exemple j'espère avoir permis à certaines personnes de mieux comprendre l'intérêt des macros et en quoi elles sont si pratique. Le code final pour cette macro est disponible ici
Notez que les read-macros ne sont pas tellement utilisées en
pratiques (on aurait très bien pu se limiter à la forme (fn (…)),
qui n'est pas tellement plus longue à taper). Néanmoins elles peuvent
être assez utiles, par exemple dans CLSQL, où
elles permettent de distinguer le code Lisp et les requêtes SQL.
Si vous souhaitez lire plus à propos des macros, je vous recommande la lecture de On Lisp de Paul Graham et de Let Over Lambda de Doug Hoyte.
