Skip to content

Let Over Macro (LOM)

Une macro dans Common Lisp est une vraie macro, entendons par là que le compilateur gén`ere le code qu’on lui demande de générer sans se poser de questions:

(defmacro print-random (i) 
                 `(let ((rr (random 10))) 
                       (format t "~A~%" (+ ,i rr))))

En plus d’être inutile, ce code en Common Lisp n’est pas correct, la variable rr peut être en conflit avec une autre variable dans contexte lexical ou cette macro est appelée. Une version correcte serait:

(defmacro print-random (i) 
                  (let ((rr (gensym))) 
                      `(let ((,rr (random 10))) 
                            (format t "~A~%" (+ ,i ,rr)))))

Ici, la variable rr est déclarée dans un champ lexical et associé à un nom unique généré par (gensym), elle est utilisée ensuite de la même façon que n’importe quelle variable passée en argument à la macro. Dans les faits, les deux versions de la macro vont fonctionner très bien, mais seule la seconde assure qu’il n’y aura pas d’effets secondaires.

Clojure est beaucoup plus stricte, il n’accepte pas l’utilisation de variables non déclarées dans ses macro et rend obligatoire l’utilisation de (gensym) dans un tel contexte. Ce qui peut poser problème lorsqu’on veut imbriquer deux macros qui utilisent la même variable.

Afin de simplifier l’utilisation de JDBC, je souhaite déclarer une macro (with-db & body) dans la quelle se trouve un environnement lexical qui me permette d’accéder directement à la base de données avec d’autres macros (fetch-db request) et (exec-db request).

(with-db (fetch-db ["select now()"]))

Le problème étant que le (with-open) utilisé par JDBC génère une variable lexicale qu’il faut reprendre dans les arguments de (jdbc/fetch) ou (jdbc/execute).

C’est ici qu’intervient le principe de LOM, ou Let Over Macro:

(let [cn (gensym)]
  (defmacro with-db [& body]
    `(with-open [~cn (jdbc/connection db)]
       ~@body))

  (defmacro exec-db [request]
    `(jdbc/execute ~cn ~request))

  (defmacro fetch-db [request]
    `(jdbc/fetch ~cn ~request)))

Le (let …) au lieu de se trouver à l’intérieur de la macro se trouve à l’extérieur, afin de créer un environnement lexical auquel toutes les macros peuvent accéder.